{
  "title": "Hover Card",
  "description": "For sighted users to preview content available behind a link with spring entrance transitions.",
  "name": "motion-hover-card",
  "dependencies": [
    "motion",
    "@base-ui/react"
  ],
  "registryDependencies": [],
  "type": "registry:ui",
  "files": [
    {
      "path": "components/paceui/motion/hover-card.tsx",
      "type": "registry:ui",
      "content": "import * as React from \"react\";\n\nimport { PreviewCard as PreviewCardPrimitive } from \"@base-ui/react/preview-card\";\nimport { AnimatePresence, type Transition, motion } from \"motion/react\";\nimport { createPortal } from \"react-dom\";\n\nimport { cn } from \"@/lib/utils\";\n\ninterface HoverCardItemData {\n    content: React.ReactNode;\n    side?: PreviewCardPrimitive.Positioner.Props[\"side\"];\n    sideOffset?: PreviewCardPrimitive.Positioner.Props[\"sideOffset\"];\n    align?: PreviewCardPrimitive.Positioner.Props[\"align\"];\n    alignOffset?: PreviewCardPrimitive.Positioner.Props[\"alignOffset\"];\n    className?: any;\n}\n\ninterface HoverCardProviderContextValue {\n    activeId: string | null;\n    openHoverCard: (id: string, element: HTMLElement, data?: HoverCardItemData | null) => void;\n    closeHoverCard: (id: string, immediate?: boolean) => void;\n    cancelClose: () => void;\n    updateHoverCardData: (id: string, data: HoverCardItemData) => void;\n}\n\nconst HoverCardProviderContext = React.createContext<HoverCardProviderContextValue | null>(null);\n\nconst HoverCardItemContext = React.createContext<{\n    id: string;\n    triggerRef: React.RefObject<HTMLElement | null>;\n    dataRef: React.RefObject<HoverCardItemData>;\n} | null>(null);\n\nexport interface HoverCardProviderProps {\n    delay?: number;\n    closeDelay?: number;\n    transition?: Transition;\n    children?: React.ReactNode;\n}\n\nconst contentVariants = {\n    initial: (offset: { x: number; y: number }) => {\n        if (offset.x === 0 && offset.y === 0) {\n            return {\n                opacity: 0,\n                scale: 0.95,\n                filter: \"blur(4px)\",\n                x: 0,\n                y: 6,\n            };\n        }\n        return {\n            opacity: 0,\n            filter: \"blur(4px)\",\n            x: offset.x * 0.5,\n            y: offset.y * 0.5,\n            scale: 0.98,\n        };\n    },\n    animate: {\n        opacity: 1,\n        filter: \"blur(0px)\",\n        x: 0,\n        y: 0,\n        scale: 1,\n    },\n    exit: (offset: { x: number; y: number }) => {\n        if (offset.x === 0 && offset.y === 0) {\n            return {\n                opacity: 0,\n                scale: 0.95,\n                filter: \"blur(4px)\",\n                y: -6,\n            };\n        }\n        return {\n            opacity: 0,\n            filter: \"blur(4px)\",\n            x: -offset.x * 0.5,\n            y: -offset.y * 0.5,\n            scale: 0.98,\n        };\n    },\n};\n\nfunction HoverCardProvider({\n    delay = 150,\n    closeDelay = 250,\n    transition = { type: \"spring\", stiffness: 380, damping: 28, mass: 0.7 },\n    children,\n}: HoverCardProviderProps) {\n    const [activeId, setActiveId] = React.useState<string | null>(null);\n    const [activeRect, setActiveRect] = React.useState<DOMRect | null>(null);\n    const [activeData, setActiveData] = React.useState<HoverCardItemData | null>(null);\n    const [offset, setOffset] = React.useState<{ x: number; y: number }>({ x: 0, y: 0 });\n    const [isMounted, setIsMounted] = React.useState(false);\n\n    const itemsDataRef = React.useRef<Map<string, HoverCardItemData>>(new Map());\n    const triggerElementsRef = React.useRef<Map<string, HTMLElement>>(new Map());\n    const activeRectRef = React.useRef<DOMRect | null>(null);\n    const closeTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);\n    const openTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);\n\n    React.useEffect(() => {\n        setIsMounted(true);\n    }, []);\n\n    const updatePosition = React.useCallback(() => {\n        if (!activeId) return;\n        const el = triggerElementsRef.current.get(activeId);\n        if (el) {\n            const rect = el.getBoundingClientRect();\n            setActiveRect(rect);\n            activeRectRef.current = rect;\n        }\n    }, [activeId]);\n\n    React.useEffect(() => {\n        if (!activeId) return;\n        window.addEventListener(\"scroll\", updatePosition, true);\n        window.addEventListener(\"resize\", updatePosition);\n        return () => {\n            window.removeEventListener(\"scroll\", updatePosition, true);\n            window.removeEventListener(\"resize\", updatePosition);\n        };\n    }, [activeId, updatePosition]);\n\n    const openHoverCard = React.useCallback(\n        (id: string, element: HTMLElement, directData?: HoverCardItemData | null) => {\n            if (closeTimeoutRef.current) {\n                clearTimeout(closeTimeoutRef.current);\n                closeTimeoutRef.current = null;\n            }\n\n            triggerElementsRef.current.set(id, element);\n            if (directData) {\n                itemsDataRef.current.set(id, directData);\n            }\n\n            const data = directData || itemsDataRef.current.get(id) || null;\n            const isAlreadyOpen = Boolean(activeId);\n            const newRect = element.getBoundingClientRect();\n\n            if (isAlreadyOpen && activeRectRef.current) {\n                const prevCenterX = activeRectRef.current.left + activeRectRef.current.width / 2;\n                const prevCenterY = activeRectRef.current.top + activeRectRef.current.height / 2;\n                const newCenterX = newRect.left + newRect.width / 2;\n                const newCenterY = newRect.top + newRect.height / 2;\n\n                const dx = prevCenterX - newCenterX;\n                const dy = prevCenterY - newCenterY;\n\n                const clampedX = Math.max(Math.min(dx, 80), -80);\n                const clampedY = Math.max(Math.min(dy, 40), -40);\n                setOffset({ x: clampedX, y: clampedY });\n            } else {\n                setOffset({ x: 0, y: 0 });\n            }\n\n            activeRectRef.current = newRect;\n\n            if (isAlreadyOpen || delay <= 0) {\n                setActiveId(id);\n                setActiveRect(newRect);\n                if (data) setActiveData(data);\n            } else {\n                if (openTimeoutRef.current) clearTimeout(openTimeoutRef.current);\n                openTimeoutRef.current = setTimeout(() => {\n                    setActiveId(id);\n                    const rect = element.getBoundingClientRect();\n                    setActiveRect(rect);\n                    activeRectRef.current = rect;\n                    const latestData = itemsDataRef.current.get(id) || directData || null;\n                    if (latestData) setActiveData(latestData);\n                }, delay);\n            }\n        },\n        [activeId, delay],\n    );\n\n    const cancelClose = React.useCallback(() => {\n        if (closeTimeoutRef.current) {\n            clearTimeout(closeTimeoutRef.current);\n            closeTimeoutRef.current = null;\n        }\n    }, []);\n\n    const closeHoverCard = React.useCallback(\n        (id: string, immediate = false) => {\n            if (openTimeoutRef.current) {\n                clearTimeout(openTimeoutRef.current);\n                openTimeoutRef.current = null;\n            }\n\n            if (closeTimeoutRef.current) clearTimeout(closeTimeoutRef.current);\n\n            const performClose = () => {\n                setActiveId((current) => {\n                    if (current === id) {\n                        setActiveRect(null);\n                        activeRectRef.current = null;\n                        setActiveData(null);\n                        setOffset({ x: 0, y: 0 });\n                        return null;\n                    }\n                    return current;\n                });\n            };\n\n            if (immediate) {\n                performClose();\n            } else {\n                closeTimeoutRef.current = setTimeout(performClose, closeDelay);\n            }\n        },\n        [closeDelay],\n    );\n\n    const updateHoverCardData = React.useCallback((id: string, data: HoverCardItemData) => {\n        itemsDataRef.current.set(id, data);\n    }, []);\n\n    const side = activeData?.side ?? \"bottom\";\n    const sideOffset = typeof activeData?.sideOffset === \"number\" ? activeData.sideOffset : 6;\n    const align = activeData?.align ?? \"center\";\n    const alignOffset = typeof activeData?.alignOffset === \"number\" ? activeData.alignOffset : 0;\n\n    let targetX = 0;\n    let targetY = 0;\n    let translateX = \"-50%\";\n    let translateY = \"0%\";\n\n    if (activeRect) {\n        if (side === \"top\") {\n            targetX =\n                align === \"start\"\n                    ? activeRect.left + alignOffset\n                    : align === \"end\"\n                      ? activeRect.right + alignOffset\n                      : activeRect.left + activeRect.width / 2 + alignOffset;\n            targetY = activeRect.top - sideOffset;\n            translateX = align === \"start\" ? \"0%\" : align === \"end\" ? \"-100%\" : \"-50%\";\n            translateY = \"-100%\";\n        } else if (side === \"bottom\") {\n            targetX =\n                align === \"start\"\n                    ? activeRect.left + alignOffset\n                    : align === \"end\"\n                      ? activeRect.right + alignOffset\n                      : activeRect.left + activeRect.width / 2 + alignOffset;\n            targetY = activeRect.bottom + sideOffset;\n            translateX = align === \"start\" ? \"0%\" : align === \"end\" ? \"-100%\" : \"-50%\";\n            translateY = \"0%\";\n        } else if (side === \"left\") {\n            targetX = activeRect.left - sideOffset;\n            targetY =\n                align === \"start\"\n                    ? activeRect.top + alignOffset\n                    : align === \"end\"\n                      ? activeRect.bottom + alignOffset\n                      : activeRect.top + activeRect.height / 2 + alignOffset;\n            translateX = \"-100%\";\n            translateY = align === \"start\" ? \"0%\" : align === \"end\" ? \"-100%\" : \"-50%\";\n        } else if (side === \"right\") {\n            targetX = activeRect.right + sideOffset;\n            targetY =\n                align === \"start\"\n                    ? activeRect.top + alignOffset\n                    : align === \"end\"\n                      ? activeRect.bottom + alignOffset\n                      : activeRect.top + activeRect.height / 2 + alignOffset;\n            translateX = \"0%\";\n            translateY = align === \"start\" ? \"0%\" : align === \"end\" ? \"-100%\" : \"-50%\";\n        }\n    }\n\n    return (\n        <HoverCardProviderContext.Provider\n            value={{\n                activeId,\n                openHoverCard,\n                closeHoverCard,\n                cancelClose,\n                updateHoverCardData,\n            }}>\n            {children}\n            {isMounted &&\n                createPortal(\n                    <AnimatePresence>\n                        {activeId && activeRect && activeData && activeData.content && (\n                            <motion.div\n                                key=\"shared-hovercard-wrapper\"\n                                initial={{\n                                    opacity: 0,\n                                    scale: 0.95,\n                                    filter: \"blur(4px)\",\n                                    x: targetX,\n                                    y: targetY,\n                                }}\n                                animate={{\n                                    opacity: 1,\n                                    scale: 1,\n                                    filter: \"blur(0px)\",\n                                    x: targetX,\n                                    y: targetY,\n                                }}\n                                exit={{\n                                    opacity: 0,\n                                    scale: 0.95,\n                                    filter: \"blur(4px)\",\n                                }}\n                                transition={transition}\n                                style={{\n                                    position: \"fixed\",\n                                    left: 0,\n                                    top: 0,\n                                    pointerEvents: \"none\",\n                                    zIndex: 99999,\n                                }}>\n                                <div\n                                    style={{\n                                        transform: `translate(${translateX}, ${translateY})`,\n                                        pointerEvents: \"auto\",\n                                    }}\n                                    onMouseEnter={cancelClose}\n                                    onMouseLeave={() => {\n                                        if (activeId) closeHoverCard(activeId);\n                                    }}>\n                                    <motion.div\n                                        layout\n                                        transition={transition}\n                                        className={cn(\n                                            \"bg-popover text-popover-foreground ring-foreground/10 z-50 w-fit max-w-sm origin-(--transform-origin) overflow-hidden rounded-md p-4 text-sm shadow-md ring-1 outline-hidden\",\n                                            activeData.className,\n                                        )}>\n                                        <div className=\"relative w-full overflow-hidden\">\n                                            <AnimatePresence mode=\"popLayout\" custom={offset} initial={false}>\n                                                <motion.div\n                                                    key={activeId}\n                                                    custom={offset}\n                                                    variants={contentVariants}\n                                                    initial=\"initial\"\n                                                    animate=\"animate\"\n                                                    exit=\"exit\"\n                                                    transition={transition}\n                                                    className=\"w-full\">\n                                                    {activeData.content}\n                                                </motion.div>\n                                            </AnimatePresence>\n                                        </div>\n                                    </motion.div>\n                                </div>\n                            </motion.div>\n                        )}\n                    </AnimatePresence>,\n                    document.body,\n                )}\n        </HoverCardProviderContext.Provider>\n    );\n}\n\nexport interface HoverCardProps extends PreviewCardPrimitive.Root.Props {\n    id?: string;\n    delay?: number;\n    closeDelay?: number;\n}\n\nfunction HoverCard({ id: propId, delay, closeDelay, children, ...props }: HoverCardProps) {\n    const parentProvider = React.useContext(HoverCardProviderContext);\n    const generatedId = React.useId();\n    const id = propId ?? generatedId;\n    const triggerRef = React.useRef<HTMLElement | null>(null);\n    const dataRef = React.useRef<HoverCardItemData>({ content: null });\n\n    if (!parentProvider) {\n        return (\n            <HoverCardProvider delay={delay} closeDelay={closeDelay}>\n                <HoverCardItemContext.Provider value={{ id, triggerRef, dataRef }}>\n                    <PreviewCardPrimitive.Root data-slot=\"hover-card\" {...props}>\n                        {children}\n                    </PreviewCardPrimitive.Root>\n                </HoverCardItemContext.Provider>\n            </HoverCardProvider>\n        );\n    }\n\n    return (\n        <HoverCardItemContext.Provider value={{ id, triggerRef, dataRef }}>\n            <PreviewCardPrimitive.Root data-slot=\"hover-card\" {...props}>\n                {children}\n            </PreviewCardPrimitive.Root>\n        </HoverCardItemContext.Provider>\n    );\n}\n\nexport interface HoverCardTriggerProps extends PreviewCardPrimitive.Trigger.Props {}\n\nfunction HoverCardTrigger({\n    className,\n    onMouseEnter,\n    onMouseLeave,\n    onPointerEnter,\n    onPointerLeave,\n    onFocus,\n    onBlur,\n    ...props\n}: HoverCardTriggerProps) {\n    const itemContext = React.useContext(HoverCardItemContext);\n    const providerContext = React.useContext(HoverCardProviderContext);\n    const localRef = React.useRef<HTMLElement | null>(null);\n\n    const handleOpen = (e: any) => {\n        if (itemContext && providerContext) {\n            const el = (e.currentTarget || itemContext.triggerRef.current || localRef.current) as HTMLElement;\n            if (el) {\n                providerContext.openHoverCard(itemContext.id, el, itemContext.dataRef.current);\n            }\n        }\n    };\n\n    const handleClose = () => {\n        if (itemContext && providerContext) {\n            providerContext.closeHoverCard(itemContext.id);\n        }\n    };\n\n    const isOpen = providerContext ? providerContext.activeId === itemContext?.id : undefined;\n\n    return (\n        <PreviewCardPrimitive.Trigger\n            data-slot=\"hover-card-trigger\"\n            data-state={isOpen ? \"open\" : isOpen === false ? \"closed\" : undefined}\n            data-open={isOpen ? \"\" : undefined}\n            ref={(node: any) => {\n                localRef.current = node;\n                if (itemContext) {\n                    itemContext.triggerRef.current = node;\n                }\n            }}\n            onMouseEnter={(e: any) => {\n                onMouseEnter?.(e);\n                handleOpen(e);\n            }}\n            onMouseLeave={(e: any) => {\n                onMouseLeave?.(e);\n                handleClose();\n            }}\n            onPointerEnter={(e: any) => {\n                onPointerEnter?.(e);\n                handleOpen(e);\n            }}\n            onPointerLeave={(e: any) => {\n                onPointerLeave?.(e);\n                handleClose();\n            }}\n            onFocus={(e: any) => {\n                onFocus?.(e);\n                handleOpen(e);\n            }}\n            onBlur={(e: any) => {\n                onBlur?.(e);\n                handleClose();\n            }}\n            className={className}\n            {...props}\n        />\n    );\n}\n\nexport interface HoverCardContentProps\n    extends\n        PreviewCardPrimitive.Popup.Props,\n        Pick<PreviewCardPrimitive.Positioner.Props, \"align\" | \"alignOffset\" | \"side\" | \"sideOffset\"> {\n    transition?: Transition;\n}\n\nfunction HoverCardContent({\n    className,\n    side = \"bottom\",\n    sideOffset = 6,\n    align = \"center\",\n    alignOffset = 0,\n    children,\n    ...props\n}: HoverCardContentProps) {\n    const itemContext = React.useContext(HoverCardItemContext);\n    const providerContext = React.useContext(HoverCardProviderContext);\n\n    const data: HoverCardItemData = {\n        content: children,\n        side,\n        sideOffset,\n        align,\n        alignOffset,\n        className,\n    };\n\n    if (itemContext) {\n        itemContext.dataRef.current = data;\n    }\n\n    React.useEffect(() => {\n        if (itemContext && providerContext) {\n            providerContext.updateHoverCardData(itemContext.id, data);\n        }\n    }, [itemContext, providerContext, children, side, sideOffset, align, alignOffset, className]);\n\n    if (providerContext) {\n        return null;\n    }\n\n    return (\n        <PreviewCardPrimitive.Portal data-slot=\"hover-card-portal\">\n            <PreviewCardPrimitive.Positioner\n                align={align}\n                alignOffset={alignOffset}\n                side={side}\n                sideOffset={sideOffset}\n                className=\"isolate z-50\">\n                <PreviewCardPrimitive.Popup\n                    data-slot=\"hover-card-content\"\n                    className={cn(\n                        \"bg-popover text-popover-foreground 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 z-50 w-64 origin-(--transform-origin) rounded-md p-4 text-sm duration-100\",\n                        className,\n                    )}\n                    {...props}>\n                    {children}\n                </PreviewCardPrimitive.Popup>\n            </PreviewCardPrimitive.Positioner>\n        </PreviewCardPrimitive.Portal>\n    );\n}\n\nfunction HoverCardArrow({ className, ...props }: PreviewCardPrimitive.Arrow.Props) {\n    return (\n        <PreviewCardPrimitive.Arrow\n            data-slot=\"hover-card-arrow\"\n            className={cn(\"fill-popover stroke-foreground/10\", className)}\n            {...props}\n        />\n    );\n}\n\nexport function useHoverCardProvider() {\n    return React.useContext(HoverCardProviderContext);\n}\n\nexport function useHoverCardItem() {\n    return React.useContext(HoverCardItemContext);\n}\n\nexport { HoverCard, HoverCardTrigger, HoverCardContent, HoverCardArrow, HoverCardProvider };\n"
    }
  ],
  "mcp": {
    "description": "For sighted users to preview content available behind a link. Provides spring directional reveal and scale entrance physics.",
    "tags": [
      "component",
      "hover-card",
      "preview-card",
      "popover",
      "paceui",
      "animation"
    ]
  }
}