{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "hover-card",
  "title": "Hover card",
  "author": "andongmin94",
  "description": "A preview card that appears when a linked element is hovered.",
  "dependencies": [
    "@base-ui/react@^1.6.0"
  ],
  "files": [
    {
      "path": "src/components/ui/hover-card.tsx",
      "content": "\"use client\";\n\nimport { PreviewCard as PreviewCardPrimitive } from \"@base-ui/react/preview-card\";\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\ntype HoverCardDelayContract = {\n  closeDelay: number;\n  openDelay: number;\n};\n\nconst HoverCardDelayContext = React.createContext<HoverCardDelayContract>({\n  closeDelay: 300,\n  openDelay: 700,\n});\nconst HoverCardOpenContext = React.createContext(false);\ntype HoverCardPortalContextValue = {\n  container: HTMLElement | null;\n  setContainer: React.Dispatch<React.SetStateAction<HTMLElement | null>>;\n};\nconst HoverCardPortalContext = React.createContext<HoverCardPortalContextValue | null>(null);\n\ntype HoverCardPointerDownOutsideEvent = CustomEvent<{ originalEvent: PointerEvent }>;\ntype HoverCardFocusOutsideEvent = CustomEvent<{ originalEvent: FocusEvent }>;\ntype HoverCardInteractOutsideEvent = HoverCardPointerDownOutsideEvent | HoverCardFocusOutsideEvent;\n\ntype HoverCardDismissHandlers = {\n  onEscapeKeyDown?: (event: KeyboardEvent) => void;\n  onFocusOutside?: (event: HoverCardFocusOutsideEvent) => void;\n  onInteractOutside?: (event: HoverCardInteractOutsideEvent) => void;\n  onPointerDownOutside?: (event: HoverCardPointerDownOutsideEvent) => void;\n};\n\nconst HoverCardDismissContext =\n  React.createContext<React.RefObject<HoverCardDismissHandlers> | null>(null);\n\ntype CollisionBoundary =\n  | PreviewCardPrimitive.Positioner.Props[\"collisionBoundary\"]\n  | null\n  | Array<Element | null>;\ntype CSSPropertiesWithVariables = React.CSSProperties & {\n  [name: `--${string}`]: string | number | undefined;\n};\ntype HoverCardPositioningProps = Pick<\n  PreviewCardPrimitive.Positioner.Props,\n  | \"align\"\n  | \"alignOffset\"\n  | \"arrowPadding\"\n  | \"collisionAvoidance\"\n  | \"collisionPadding\"\n  | \"disableAnchorTracking\"\n  | \"positionMethod\"\n  | \"side\"\n  | \"sideOffset\"\n> & {\n  avoidCollisions?: boolean;\n  collisionBoundary?: CollisionBoundary;\n  hideWhenDetached?: boolean;\n  sticky?: \"partial\" | \"always\";\n  updatePositionStrategy?: \"optimized\" | \"always\";\n};\n\nconst hoverCardCssVariables: CSSPropertiesWithVariables = {\n  \"--radix-hover-card-content-available-height\": \"var(--available-height)\",\n  \"--radix-hover-card-content-available-width\": \"var(--available-width)\",\n  \"--radix-hover-card-content-transform-origin\": \"var(--transform-origin)\",\n  \"--radix-hover-card-trigger-height\": \"var(--anchor-height)\",\n  \"--radix-hover-card-trigger-width\": \"var(--anchor-width)\",\n};\n\nfunction mergePopupStyle(\n  style: PreviewCardPrimitive.Popup.Props[\"style\"],\n): PreviewCardPrimitive.Popup.Props[\"style\"] {\n  if (typeof style === \"function\") {\n    return (state) => ({ ...hoverCardCssVariables, ...style(state) });\n  }\n\n  return { ...hoverCardCssVariables, ...style };\n}\n\nfunction normalizeCollisionBoundary(\n  collisionBoundary: CollisionBoundary | undefined,\n): PreviewCardPrimitive.Positioner.Props[\"collisionBoundary\"] {\n  if (Array.isArray(collisionBoundary)) {\n    const boundaries: Element[] = [];\n    for (const boundary of collisionBoundary) {\n      if (boundary) boundaries.push(boundary);\n    }\n    return boundaries;\n  }\n\n  return collisionBoundary ?? undefined;\n}\n\nfunction preserveRadixEventCancellation(\n  child: React.ReactElement<{ [key: string]: unknown; children?: React.ReactNode }>,\n) {\n  const eventProps: Record<string, unknown> = {};\n\n  for (const [name, handler] of Object.entries(child.props)) {\n    if (/^on[A-Z]/.test(name) && typeof handler === \"function\") {\n      eventProps[name] = (...args: unknown[]) => {\n        (handler as (...handlerArgs: unknown[]) => void)(...args);\n        const event = args[0] as\n          | { defaultPrevented?: boolean; preventBaseUIHandler?: () => void }\n          | undefined;\n        if (event?.defaultPrevented) event.preventBaseUIHandler?.();\n      };\n    }\n  }\n\n  return React.cloneElement(child, eventProps);\n}\n\nfunction getAsChildElement(children: React.ReactNode, componentName: string) {\n  const child = React.Children.toArray(children).find(React.isValidElement);\n\n  if (!child) {\n    throw new Error(`${componentName} with asChild requires a valid React element child.`);\n  }\n\n  return preserveRadixEventCancellation(\n    child as React.ReactElement<{\n      [key: string]: unknown;\n      children?: React.ReactNode;\n    }>,\n  );\n}\n\nfunction setRefValue<T>(ref: React.Ref<T> | undefined, value: T | null) {\n  if (typeof ref === \"function\") {\n    ref(value);\n  } else if (ref) {\n    ref.current = value;\n  }\n}\n\nfunction toPointerEvent(originalEvent: Event) {\n  if (originalEvent instanceof PointerEvent) {\n    return originalEvent;\n  }\n\n  const mouseEvent = originalEvent instanceof MouseEvent ? originalEvent : undefined;\n  return new PointerEvent(originalEvent.type, {\n    altKey: mouseEvent?.altKey,\n    bubbles: originalEvent.bubbles,\n    button: mouseEvent?.button,\n    buttons: mouseEvent?.buttons,\n    cancelable: originalEvent.cancelable,\n    clientX: mouseEvent?.clientX,\n    clientY: mouseEvent?.clientY,\n    composed: originalEvent.composed,\n    ctrlKey: mouseEvent?.ctrlKey,\n    metaKey: mouseEvent?.metaKey,\n    pointerType:\n      typeof TouchEvent !== \"undefined\" && originalEvent instanceof TouchEvent ? \"touch\" : \"mouse\",\n    shiftKey: mouseEvent?.shiftKey,\n  });\n}\n\nfunction createPointerDownOutsideEvent(name: string, originalEvent: Event) {\n  const event = new CustomEvent<{ originalEvent: PointerEvent }>(name, {\n    cancelable: true,\n    detail: { originalEvent: toPointerEvent(originalEvent) },\n  });\n\n  Object.defineProperty(event, \"target\", {\n    configurable: true,\n    value: originalEvent.target,\n  });\n\n  return event;\n}\n\nfunction HoverCard({\n  closeDelay = 300,\n  defaultOpen = false,\n  onOpenChange,\n  open: openProp,\n  openDelay = 700,\n  ...props\n}: PreviewCardPrimitive.Root.Props & {\n  closeDelay?: number;\n  openDelay?: number;\n}) {\n  const dismissHandlersRef = React.useRef<HoverCardDismissHandlers>({});\n  const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen);\n  const [container, setContainer] = React.useState<HTMLElement | null>(null);\n  const open = openProp ?? uncontrolledOpen;\n  const portalContext = React.useMemo(() => ({ container, setContainer }), [container]);\n\n  return (\n    <HoverCardPortalContext.Provider value={portalContext}>\n      <HoverCardDelayContext.Provider value={{ closeDelay, openDelay }}>\n        <HoverCardOpenContext.Provider value={open}>\n          <HoverCardDismissContext.Provider value={dismissHandlersRef}>\n            <PreviewCardPrimitive.Root\n              data-slot=\"hover-card\"\n              defaultOpen={defaultOpen}\n              open={openProp}\n              onOpenChange={(nextOpen, eventDetails) => {\n                if (!nextOpen && eventDetails.reason === \"escape-key\") {\n                  const keyboardEvent = eventDetails.event;\n                  if (keyboardEvent instanceof KeyboardEvent) {\n                    dismissHandlersRef.current.onEscapeKeyDown?.(keyboardEvent);\n                    if (keyboardEvent.defaultPrevented) {\n                      eventDetails.cancel();\n                    }\n                  }\n                } else if (!nextOpen && eventDetails.reason === \"outside-press\") {\n                  const pointerEvent = createPointerDownOutsideEvent(\n                    \"hoverCard.pointerDownOutside\",\n                    eventDetails.event,\n                  );\n                  dismissHandlersRef.current.onPointerDownOutside?.(pointerEvent);\n                  dismissHandlersRef.current.onInteractOutside?.(pointerEvent);\n                  if (pointerEvent.defaultPrevented) {\n                    eventDetails.cancel();\n                  }\n                }\n\n                if (!eventDetails.isCanceled) {\n                  onOpenChange?.(nextOpen, eventDetails);\n                }\n                if (!eventDetails.isCanceled && openProp === undefined) {\n                  setUncontrolledOpen(nextOpen);\n                }\n              }}\n              {...props}\n            />\n          </HoverCardDismissContext.Provider>\n        </HoverCardOpenContext.Provider>\n      </HoverCardDelayContext.Provider>\n    </HoverCardPortalContext.Provider>\n  );\n}\n\nconst HoverCardTrigger = React.forwardRef<\n  HTMLElement,\n  PreviewCardPrimitive.Trigger.Props & {\n    asChild?: boolean;\n    children?: React.ReactNode;\n  }\n>(function HoverCardTrigger(\n  { asChild = false, children, closeDelay, delay, render, ...props },\n  forwardedRef,\n) {\n  const rootDelay = React.useContext(HoverCardDelayContext);\n  const open = React.useContext(HoverCardOpenContext);\n  const portalContext = React.useContext(HoverCardPortalContext);\n  const setPortalContainer = portalContext?.setContainer;\n  const renderElement = asChild ? getAsChildElement(children, \"HoverCardTrigger\") : render;\n  const mergedRef = React.useCallback(\n    (trigger: HTMLElement | null) => {\n      setRefValue(forwardedRef, trigger);\n      const nextContainer = trigger?.parentElement ?? null;\n      setPortalContainer?.((current) => (current === nextContainer ? current : nextContainer));\n    },\n    [forwardedRef, setPortalContainer],\n  );\n\n  return (\n    <PreviewCardPrimitive.Trigger\n      ref={mergedRef}\n      data-slot=\"hover-card-trigger\"\n      data-state={open ? \"open\" : \"closed\"}\n      closeDelay={closeDelay ?? rootDelay.closeDelay}\n      delay={delay ?? rootDelay.openDelay}\n      render={renderElement}\n      {...props}\n    >\n      {asChild ? undefined : children}\n    </PreviewCardPrimitive.Trigger>\n  );\n});\n\ntype HoverCardContentProps = PreviewCardPrimitive.Popup.Props &\n  HoverCardPositioningProps & {\n    asChild?: boolean;\n    forceMount?: true;\n    keepMounted?: boolean;\n    onEscapeKeyDown?: (event: KeyboardEvent) => void;\n    onFocusOutside?: (event: HoverCardFocusOutsideEvent) => void;\n    onInteractOutside?: (event: HoverCardInteractOutsideEvent) => void;\n    onPointerDownOutside?: (event: HoverCardPointerDownOutsideEvent) => void;\n  };\n\nconst HoverCardContent = React.forwardRef<HTMLDivElement, HoverCardContentProps>(\n  function HoverCardContent(\n    {\n      className,\n      arrowPadding = 0,\n      asChild = false,\n      avoidCollisions = true,\n      children,\n      collisionAvoidance,\n      collisionBoundary,\n      collisionPadding = 0,\n      disableAnchorTracking,\n      forceMount,\n      hideWhenDetached = false,\n      keepMounted,\n      side = \"bottom\",\n      sideOffset = 4,\n      align = \"center\",\n      alignOffset = 0,\n      onEscapeKeyDown,\n      onFocusOutside,\n      onInteractOutside,\n      onPointerDownOutside,\n      positionMethod = \"fixed\",\n      render,\n      sticky,\n      style,\n      updatePositionStrategy,\n      ...props\n    },\n    forwardedRef,\n  ) {\n    const dismissHandlersRef = React.useContext(HoverCardDismissContext);\n    const open = React.useContext(HoverCardOpenContext);\n    const portalContext = React.useContext(HoverCardPortalContext);\n    const renderElement = asChild ? getAsChildElement(children, \"HoverCardContent\") : render;\n    const [popupElement, setPopupElement] = React.useState<HTMLDivElement | null>(null);\n    const mergedRef = React.useCallback(\n      (popup: HTMLDivElement | null) => {\n        setPopupElement(popup);\n        setRefValue(forwardedRef, popup);\n      },\n      [forwardedRef],\n    );\n\n    React.useLayoutEffect(() => {\n      if (dismissHandlersRef === null) {\n        return undefined;\n      }\n\n      const handlers = {\n        onEscapeKeyDown,\n        onFocusOutside,\n        onInteractOutside,\n        onPointerDownOutside,\n      };\n      dismissHandlersRef.current = handlers;\n      return () => {\n        if (dismissHandlersRef.current === handlers) {\n          dismissHandlersRef.current = {};\n        }\n      };\n    }, [\n      dismissHandlersRef,\n      onEscapeKeyDown,\n      onFocusOutside,\n      onInteractOutside,\n      onPointerDownOutside,\n    ]);\n\n    React.useEffect(() => {\n      if (popupElement === null) {\n        return undefined;\n      }\n\n      const disableTabStops = () => {\n        const candidates = popupElement.querySelectorAll<HTMLElement>(\n          \"a[href], button, input, select, textarea, [tabindex]\",\n        );\n\n        candidates.forEach((candidate) => {\n          candidate.tabIndex = -1;\n        });\n      };\n\n      disableTabStops();\n      const observer = new MutationObserver(disableTabStops);\n      observer.observe(popupElement, { childList: true, subtree: true });\n\n      return () => observer.disconnect();\n    }, [popupElement]);\n\n    return (\n      <PreviewCardPrimitive.Portal\n        container={portalContext?.container}\n        data-slot=\"hover-card-portal\"\n        keepMounted={keepMounted ?? forceMount}\n      >\n        <PreviewCardPrimitive.Positioner\n          align={align}\n          alignOffset={alignOffset}\n          arrowPadding={arrowPadding}\n          collisionAvoidance={\n            avoidCollisions\n              ? collisionAvoidance\n              : { align: \"none\", fallbackAxisSide: \"none\", side: \"none\" }\n          }\n          collisionBoundary={normalizeCollisionBoundary(collisionBoundary)}\n          collisionPadding={collisionPadding}\n          disableAnchorTracking={\n            updatePositionStrategy === \"always\" ? false : disableAnchorTracking\n          }\n          positionMethod={positionMethod}\n          side={side}\n          sideOffset={sideOffset}\n          sticky={sticky === undefined ? undefined : sticky === \"always\"}\n          className={cn(\n            \"isolate z-50\",\n            hideWhenDetached &&\n              \"data-anchor-hidden:pointer-events-none data-anchor-hidden:invisible\",\n          )}\n        >\n          <PreviewCardPrimitive.Popup\n            ref={mergedRef}\n            data-slot=\"hover-card-content\"\n            data-state={open ? \"open\" : \"closed\"}\n            render={renderElement}\n            style={mergePopupStyle(style)}\n            className={cn(\n              \"z-50 w-64 origin-(--transform-origin) rounded-base border-2 border-border bg-main p-4 font-base text-main-foreground outline-hidden duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95\",\n              className,\n            )}\n            {...props}\n          >\n            {asChild ? undefined : children}\n          </PreviewCardPrimitive.Popup>\n        </PreviewCardPrimitive.Positioner>\n      </PreviewCardPrimitive.Portal>\n    );\n  },\n);\n\nexport { HoverCard, HoverCardTrigger, HoverCardContent };\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "overlay",
    "data-display"
  ],
  "type": "registry:ui"
}