Radix popover with origin-aware scale from the trigger side, 150ms enter / 100ms exit, overlay shadow, and an optional arrow.
npx shadcn@latest add @paragon/popover"use client";
import * as React from "react";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import { cn } from "@/lib/utils";
/*
* Origin-aware popover physics: the content scales 0.97 -> 1 from the
* trigger side (transform-origin comes from Radix's positioning var),
* 150ms in with ease-out, 100ms out with ease-exit settling at 0.99 —
* the exit never retraces the enter. Reduced motion keeps only the fade.
*/
const popoverStyles = `
@keyframes pg-popover-in { from { opacity: 0; scale: 0.97; } }
@keyframes pg-popover-out { to { opacity: 0; scale: 0.99; } }
@media (prefers-reduced-motion: reduce) {
@keyframes pg-popover-in { from { opacity: 0; } }
@keyframes pg-popover-out { to { opacity: 0; } }
}
`;
function Popover(props: React.ComponentProps<typeof PopoverPrimitive.Root>) {
return <PopoverPrimitive.Root data-slot="popover" {...props} />;
}
function PopoverTrigger(
props: React.ComponentProps<typeof PopoverPrimitive.Trigger>,
) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
}
function PopoverAnchor(
props: React.ComponentProps<typeof PopoverPrimitive.Anchor>,
) {
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />;
}
export interface PopoverContentProps
extends React.ComponentProps<typeof PopoverPrimitive.Content> {
/** Render a small arrow pointing at the trigger. */
showArrow?: boolean;
}
function PopoverContent({
className,
align = "center",
sideOffset = 6,
showArrow = false,
children,
...props
}: PopoverContentProps) {
return (
<>
{/* Hoisted outside the Portal: React 19 does not reliably render a
bare <style> inside a Portal subtree, dropping the keyframes. */}
<style href="paragon-popover" precedence="paragon">{popoverStyles}</style>
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
data-slot="popover-content"
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-lg bg-popover p-4 text-popover-foreground shadow-overlay outline-none",
"data-[state=open]:animate-[pg-popover-in_150ms_var(--ease-out)]",
"data-[state=closed]:animate-[pg-popover-out_100ms_var(--ease-exit)_forwards]",
className,
)}
{...props}
>
{children}
{showArrow && (
<PopoverPrimitive.Arrow
width={12}
height={6}
className="fill-popover"
/>
)}
</PopoverPrimitive.Content>
</PopoverPrimitive.Portal>
</>
);
}
export { Popover, PopoverTrigger, PopoverAnchor, PopoverContent };