{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"hover-card","type":"registry:ui","title":"Hover Card","description":"Rich preview card that blooms out of a link on hover or focus, with open/close delays, an arrow, smart flipping and a top-layer render.","dependencies":[],"registryDependencies":["utils"],"files":[{"path":"registry/ui/hover-card.tsx","type":"registry:ui","content":"\"use client\";\n\nimport {\n  createContext,\n  useContext,\n  useEffect,\n  useId,\n  useRef,\n  useState,\n  type AnchorHTMLAttributes,\n  type HTMLAttributes,\n  type ReactNode,\n  type RefObject,\n} from \"react\";\nimport { cn } from \"@/lib/utils\";\n\n// Content is a native `popover=\"manual\"`: rendered in the top layer (never clipped),\n// but opening/closing is ours, driven by hover and focus with delays.\nconst css = `\n.ui-hovercard{position:fixed;inset:auto;margin:0;overflow:visible;opacity:0;scale:.92;filter:blur(3px);translate:var(--from,0 0);transition:opacity .15s ease-in,scale .15s ease-in,filter .15s,translate .15s ease-in,display .15s allow-discrete,overlay .15s allow-discrete}\n.ui-hovercard:popover-open{opacity:1;scale:1;filter:blur(0);translate:0 0;transition-duration:.3s;transition-timing-function:cubic-bezier(.2,.9,.3,1.1)}\n@starting-style{.ui-hovercard:popover-open{opacity:0;scale:.92;filter:blur(3px);translate:var(--from,0 0)}}\n.ui-hovercard[data-side=bottom]{--from:0 -10px;transform-origin:var(--arrow) top}\n.ui-hovercard[data-side=top]{--from:0 10px;transform-origin:var(--arrow) bottom}\n.ui-hovercard[data-side=left]{--from:10px 0;transform-origin:right var(--arrow)}\n.ui-hovercard[data-side=right]{--from:-10px 0;transform-origin:left var(--arrow)}\n.ui-hovercard-arrow{position:absolute;width:10px;height:10px;rotate:45deg;background:inherit;border:inherit}\n.ui-hovercard[data-side=bottom]>.ui-hovercard-arrow{top:-6px;left:calc(var(--arrow) - 5px);border-right:0;border-bottom:0}\n.ui-hovercard[data-side=top]>.ui-hovercard-arrow{bottom:-6px;left:calc(var(--arrow) - 5px);border-left:0;border-top:0}\n.ui-hovercard[data-side=right]>.ui-hovercard-arrow{left:-6px;top:calc(var(--arrow) - 5px);border-right:0;border-top:0}\n.ui-hovercard[data-side=left]>.ui-hovercard-arrow{right:-6px;top:calc(var(--arrow) - 5px);border-left:0;border-bottom:0}\n@media (prefers-reduced-motion:reduce){.ui-hovercard{transition-duration:0s!important}}\n`;\n\ntype Side = \"top\" | \"right\" | \"bottom\" | \"left\";\ntype Align = \"start\" | \"center\" | \"end\";\nconst OPPOSITE = { top: \"bottom\", bottom: \"top\", left: \"right\", right: \"left\" } as const;\n\n/** Position a fixed element beside an anchor: flip if the preferred side lacks room, then shift into the viewport. */\nfunction place(anchor: DOMRect, el: HTMLElement, preferred: Side, align: Align, offset: number, pad = 8) {\n  const w = el.offsetWidth;\n  const h = el.offsetHeight;\n  const vw = document.documentElement.clientWidth;\n  const vh = document.documentElement.clientHeight;\n  const room = { top: anchor.top, bottom: vh - anchor.bottom, left: anchor.left, right: vw - anchor.right };\n  let side: Side = preferred;\n  const vertical0 = side === \"top\" || side === \"bottom\";\n  if (room[side] < (vertical0 ? h : w) + offset + pad && room[OPPOSITE[side]] > room[side]) side = OPPOSITE[side];\n  const vertical = side === \"top\" || side === \"bottom\";\n  const along = (start: number, size: number, len: number) =>\n    align === \"start\" ? start : align === \"end\" ? start + size - len : start + size / 2 - len / 2;\n  let x = vertical ? along(anchor.left, anchor.width, w) : side === \"right\" ? anchor.right + offset : anchor.left - offset - w;\n  let y = vertical ? (side === \"bottom\" ? anchor.bottom + offset : anchor.top - offset - h) : along(anchor.top, anchor.height, h);\n  x = Math.min(Math.max(pad, x), vw - w - pad);\n  y = Math.min(Math.max(pad, y), vh - h - pad);\n  const arrow = vertical ? anchor.left + anchor.width / 2 - x : anchor.top + anchor.height / 2 - y;\n  el.style.left = `${x}px`;\n  el.style.top = `${y}px`;\n  el.dataset.side = side;\n  el.style.setProperty(\"--arrow\", `${Math.min(Math.max(14, arrow), (vertical ? w : h) - 14)}px`);\n}\n\ntype Ctx = {\n  open: boolean;\n  setOpen: (open: boolean) => void;\n  /** Open or close after the configured delay; a later call cancels an earlier one. */\n  schedule: (open: boolean) => void;\n  triggerRef: RefObject<HTMLAnchorElement | null>;\n  contentId: string;\n};\nconst HoverCardContext = createContext<Ctx | null>(null);\n\nfunction useHoverCard() {\n  const ctx = useContext(HoverCardContext);\n  if (!ctx) throw new Error(\"HoverCard parts must be rendered inside <HoverCard>.\");\n  return ctx;\n}\n\nexport interface HoverCardProps {\n  /** ms of hover/focus before opening. */\n  openDelay?: number;\n  /** ms after leaving before closing (lets the pointer travel to the card). */\n  closeDelay?: number;\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  children?: ReactNode;\n}\n\nexport function HoverCard({ openDelay = 500, closeDelay = 250, open: openProp, defaultOpen = false, onOpenChange, children }: HoverCardProps) {\n  const [inner, setInner] = useState(defaultOpen);\n  const open = openProp ?? inner;\n  const triggerRef = useRef<HTMLAnchorElement>(null);\n  const timer = useRef(0);\n  const id = useId();\n\n  const setOpen = (next: boolean) => {\n    clearTimeout(timer.current);\n    if (next === open) return;\n    if (openProp === undefined) setInner(next);\n    onOpenChange?.(next);\n  };\n  const schedule = (next: boolean) => {\n    clearTimeout(timer.current);\n    timer.current = window.setTimeout(() => setOpen(next), next ? openDelay : closeDelay);\n  };\n  useEffect(() => () => clearTimeout(timer.current), []);\n\n  return <HoverCardContext value={{ open, setOpen, schedule, triggerRef, contentId: `${id}-card` }}>{children}</HoverCardContext>;\n}\n\nexport function HoverCardTrigger({ className, onPointerEnter, onPointerLeave, onFocus, onBlur, ...props }: AnchorHTMLAttributes<HTMLAnchorElement>) {\n  const { open, schedule, triggerRef, contentId } = useHoverCard();\n  return (\n    <a\n      {...props}\n      ref={triggerRef}\n      aria-describedby={open ? contentId : undefined}\n      onPointerEnter={(e) => {\n        onPointerEnter?.(e);\n        if (e.pointerType !== \"touch\") schedule(true);\n      }}\n      onPointerLeave={(e) => {\n        onPointerLeave?.(e);\n        if (e.pointerType !== \"touch\") schedule(false);\n      }}\n      onFocus={(e) => {\n        onFocus?.(e);\n        schedule(true);\n      }}\n      onBlur={(e) => {\n        onBlur?.(e);\n        schedule(false);\n      }}\n      className={cn(\n        \"rounded-sm font-medium text-foreground underline decoration-primary/40 decoration-2 underline-offset-4 outline-none transition-[text-decoration-color] hover:decoration-primary focus-visible:ring-2 focus-visible:ring-ring/60\",\n        className,\n      )}\n    />\n  );\n}\n\nexport interface HoverCardContentProps extends HTMLAttributes<HTMLDivElement> {\n  /** Preferred side; flips automatically when there is no room. */\n  side?: Side;\n  align?: Align;\n  sideOffset?: number;\n  arrow?: boolean;\n}\n\nexport function HoverCardContent({\n  side = \"bottom\",\n  align = \"center\",\n  sideOffset = 10,\n  arrow = true,\n  className,\n  children,\n  onPointerEnter,\n  onPointerLeave,\n  ...props\n}: HoverCardContentProps) {\n  const { open, setOpen, schedule, triggerRef, contentId } = useHoverCard();\n  const ref = useRef<HTMLDivElement>(null);\n  const close = useRef(setOpen);\n  useEffect(() => {\n    close.current = setOpen;\n  });\n\n  useEffect(() => {\n    const el = ref.current;\n    if (!el) return;\n    if (!open) {\n      if (el.matches(\":popover-open\")) el.hidePopover();\n      return;\n    }\n    if (!el.matches(\":popover-open\")) el.showPopover();\n    const position = () => {\n      const t = triggerRef.current;\n      if (t) place(t.getBoundingClientRect(), el, side, align, sideOffset);\n    };\n    const onKey = (e: KeyboardEvent) => e.key === \"Escape\" && close.current(false);\n    position();\n    window.addEventListener(\"scroll\", position, { capture: true, passive: true });\n    window.addEventListener(\"resize\", position);\n    document.addEventListener(\"keydown\", onKey);\n    return () => {\n      window.removeEventListener(\"scroll\", position, { capture: true });\n      window.removeEventListener(\"resize\", position);\n      document.removeEventListener(\"keydown\", onKey);\n    };\n  }, [open, side, align, sideOffset, triggerRef]);\n\n  return (\n    <div\n      {...props}\n      ref={ref}\n      id={contentId}\n      popover=\"manual\"\n      data-side={side}\n      onPointerEnter={(e) => {\n        onPointerEnter?.(e);\n        if (e.pointerType !== \"touch\") schedule(true);\n      }}\n      onPointerLeave={(e) => {\n        onPointerLeave?.(e);\n        if (e.pointerType !== \"touch\") schedule(false);\n      }}\n      className={cn(\n        \"ui-hovercard z-50 w-72 rounded-2xl border border-border bg-popover p-4 text-sm text-popover-foreground shadow-2xl shadow-black/25 outline-none\",\n        className,\n      )}\n    >\n      <style href=\"ui-hover-card\" precedence=\"default\">\n        {css}\n      </style>\n      {arrow && <span aria-hidden className=\"ui-hovercard-arrow\" />}\n      {children}\n    </div>\n  );\n}\n"}]}