{
  "title": "Tooltip",
  "description": "A popup that displays information related to an element when the element receives keyboard focus or the mouse hovers over it, with shared provider morphing and blur-replace animations.",
  "name": "motion-tooltip",
  "dependencies": [
    "motion",
    "@base-ui/react",
    "lucide-react"
  ],
  "registryDependencies": [],
  "type": "registry:ui",
  "files": [
    {
      "path": "components/paceui/motion/tooltip.tsx",
      "type": "registry:ui",
      "content": "import * as React from \"react\";\n\nimport { Tooltip as TooltipPrimitive } from \"@base-ui/react/tooltip\";\nimport { AnimatePresence, type Transition, motion } from \"motion/react\";\nimport { createPortal } from \"react-dom\";\n\nimport { cn } from \"@/lib/utils\";\n\ninterface TooltipItemData {\n    content: React.ReactNode;\n    side?: TooltipPrimitive.Positioner.Props[\"side\"];\n    sideOffset?: TooltipPrimitive.Positioner.Props[\"sideOffset\"];\n    align?: TooltipPrimitive.Positioner.Props[\"align\"];\n    alignOffset?: TooltipPrimitive.Positioner.Props[\"alignOffset\"];\n    className?: string | ((state: any) => string | undefined);\n    showArrow?: boolean;\n}\n\ninterface TooltipProviderContextValue {\n    activeId: string | null;\n    openTooltip: (id: string, element: HTMLElement, data?: TooltipItemData | null) => void;\n    closeTooltip: (id: string) => void;\n    updateTooltipData: (id: string, data: TooltipItemData) => void;\n}\n\nconst TooltipProviderContext = React.createContext<TooltipProviderContextValue | null>(null);\n\nconst TooltipItemContext = React.createContext<{\n    id: string;\n    triggerRef: React.RefObject<HTMLElement | null>;\n    dataRef: React.RefObject<TooltipItemData>;\n} | null>(null);\n\nexport interface TooltipProviderProps extends TooltipPrimitive.Provider.Props {\n    transition?: Transition;\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.96,\n                filter: \"blur(4px)\",\n                x: 0,\n                y: 4,\n            };\n        }\n        return {\n            opacity: 0,\n            filter: \"blur(4px)\",\n            x: offset.x,\n            y: offset.y,\n            scale: 0.96,\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.96,\n                filter: \"blur(4px)\",\n                x: 0,\n                y: -4,\n            };\n        }\n        return {\n            opacity: 0,\n            filter: \"blur(4px)\",\n            x: -offset.x * 0.6,\n            y: -offset.y * 0.6,\n            scale: 0.96,\n        };\n    },\n};\n\nfunction TooltipProvider({\n    delay = 0,\n    closeDelay = 150,\n    transition = { type: \"spring\", stiffness: 420, damping: 28, mass: 0.7 },\n    children,\n    ...props\n}: TooltipProviderProps) {\n    const [activeId, setActiveId] = React.useState<string | null>(null);\n    const [activeRect, setActiveRect] = React.useState<DOMRect | null>(null);\n    const [activeData, setActiveData] = React.useState<TooltipItemData | 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, TooltipItemData>>(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 openTooltip = React.useCallback(\n        (id: string, element: HTMLElement, directData?: TooltipItemData | 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 closeTooltip = React.useCallback(\n        (id: string) => {\n            if (openTimeoutRef.current) {\n                clearTimeout(openTimeoutRef.current);\n                openTimeoutRef.current = null;\n            }\n\n            if (closeTimeoutRef.current) clearTimeout(closeTimeoutRef.current);\n            closeTimeoutRef.current = setTimeout(() => {\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            }, closeDelay);\n        },\n        [closeDelay],\n    );\n\n    const updateTooltipData = React.useCallback((id: string, data: TooltipItemData) => {\n        itemsDataRef.current.set(id, data);\n    }, []);\n\n    const side = activeData?.side ?? \"top\";\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 = \"-100%\";\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        <TooltipProviderContext.Provider\n            value={{\n                activeId,\n                openTooltip,\n                closeTooltip,\n                updateTooltipData,\n            }}>\n            <TooltipPrimitive.Provider data-slot=\"tooltip-provider\" delay={delay} {...props}>\n                {children}\n            </TooltipPrimitive.Provider>\n            {isMounted &&\n                createPortal(\n                    <AnimatePresence>\n                        {activeId && activeRect && activeData && activeData.content && (\n                            <motion.div\n                                key=\"shared-tooltip-wrapper\"\n                                initial={{\n                                    opacity: 0,\n                                    scale: 0.94,\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.94,\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                                    }}>\n                                    <motion.div\n                                        layout\n                                        transition={transition}\n                                        className={cn(\n                                            \"bg-foreground text-background relative w-fit max-w-xs rounded-md px-3 py-1.5 text-xs shadow-md select-none\",\n                                            activeData.className,\n                                        )}>\n                                        <div className=\"relative flex items-center justify-center 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=\"flex items-center gap-1.5 whitespace-nowrap\">\n                                                    {activeData.content}\n                                                </motion.div>\n                                            </AnimatePresence>\n                                        </div>\n                                        {activeData.showArrow !== false && (\n                                            <div\n                                                className={cn(\n                                                    \"bg-foreground pointer-events-none absolute size-2 rotate-45 rounded-[1px]\",\n                                                    side === \"top\" && \"-bottom-1 left-1/2 -translate-x-1/2\",\n                                                    side === \"bottom\" && \"-top-1 left-1/2 -translate-x-1/2\",\n                                                    side === \"left\" && \"top-1/2 -right-1 -translate-y-1/2\",\n                                                    side === \"right\" && \"top-1/2 -left-1 -translate-y-1/2\",\n                                                )}\n                                            />\n                                        )}\n                                    </motion.div>\n                                </div>\n                            </motion.div>\n                        )}\n                    </AnimatePresence>,\n                    document.body,\n                )}\n        </TooltipProviderContext.Provider>\n    );\n}\n\nexport interface TooltipProps extends TooltipPrimitive.Root.Props {\n    id?: string;\n}\n\nfunction Tooltip({ id: propId, children, ...props }: TooltipProps) {\n    const parentProvider = React.useContext(TooltipProviderContext);\n    const generatedId = React.useId();\n    const id = propId ?? generatedId;\n    const triggerRef = React.useRef<HTMLElement | null>(null);\n    const dataRef = React.useRef<TooltipItemData>({ content: null });\n\n    if (!parentProvider) {\n        return (\n            <TooltipProvider>\n                <TooltipItemContext.Provider value={{ id, triggerRef, dataRef }}>\n                    <TooltipPrimitive.Root data-slot=\"tooltip\" {...props}>\n                        {children}\n                    </TooltipPrimitive.Root>\n                </TooltipItemContext.Provider>\n            </TooltipProvider>\n        );\n    }\n\n    return (\n        <TooltipItemContext.Provider value={{ id, triggerRef, dataRef }}>\n            <TooltipPrimitive.Root data-slot=\"tooltip\" {...props}>\n                {children}\n            </TooltipPrimitive.Root>\n        </TooltipItemContext.Provider>\n    );\n}\n\nexport interface TooltipTriggerProps extends TooltipPrimitive.Trigger.Props {}\n\nfunction TooltipTrigger({\n    className,\n    onMouseEnter,\n    onMouseLeave,\n    onPointerEnter,\n    onPointerLeave,\n    onFocus,\n    onBlur,\n    ...props\n}: TooltipTriggerProps) {\n    const itemContext = React.useContext(TooltipItemContext);\n    const providerContext = React.useContext(TooltipProviderContext);\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.openTooltip(itemContext.id, el, itemContext.dataRef.current);\n            }\n        }\n    };\n\n    const handleClose = () => {\n        if (itemContext && providerContext) {\n            providerContext.closeTooltip(itemContext.id);\n        }\n    };\n\n    return (\n        <TooltipPrimitive.Trigger\n            data-slot=\"tooltip-trigger\"\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 TooltipContentProps\n    extends\n        TooltipPrimitive.Popup.Props,\n        Pick<TooltipPrimitive.Positioner.Props, \"align\" | \"alignOffset\" | \"side\" | \"sideOffset\"> {\n    transition?: Transition;\n    showArrow?: boolean;\n}\n\nfunction TooltipContent({\n    className,\n    side = \"top\",\n    sideOffset = 6,\n    align = \"center\",\n    alignOffset = 0,\n    showArrow = true,\n    children,\n    ...props\n}: TooltipContentProps) {\n    const itemContext = React.useContext(TooltipItemContext);\n    const providerContext = React.useContext(TooltipProviderContext);\n\n    const data: TooltipItemData = {\n        content: children,\n        side,\n        sideOffset,\n        align,\n        alignOffset,\n        className,\n        showArrow,\n    };\n\n    if (itemContext) {\n        itemContext.dataRef.current = data;\n    }\n\n    React.useEffect(() => {\n        if (itemContext && providerContext) {\n            providerContext.updateTooltipData(itemContext.id, data);\n        }\n    }, [itemContext, providerContext, children, side, sideOffset, align, alignOffset, className, showArrow]);\n\n    if (providerContext) {\n        return null;\n    }\n\n    return (\n        <TooltipPrimitive.Portal>\n            <TooltipPrimitive.Positioner\n                align={align}\n                alignOffset={alignOffset}\n                side={side}\n                sideOffset={sideOffset}\n                className=\"isolate z-50\">\n                <TooltipPrimitive.Popup\n                    data-slot=\"tooltip-content\"\n                    className={cn(\n                        \"data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 data-[side=bottom]:slide-in-from-top-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 bg-foreground text-background z-50 w-fit max-w-xs origin-(--transform-origin) rounded-md px-3 py-1.5 text-xs shadow-md\",\n                        className,\n                    )}\n                    {...props}>\n                    {children}\n                    {showArrow && (\n                        <TooltipPrimitive.Arrow className=\"bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px] data-[side=bottom]:top-1 data-[side=left]:top-1/2! data-[side=left]:-right-1 data-[side=left]:-translate-y-1/2 data-[side=right]:top-1/2! data-[side=right]:-left-1 data-[side=right]:-translate-y-1/2 data-[side=top]:-bottom-2.5\" />\n                    )}\n                </TooltipPrimitive.Popup>\n            </TooltipPrimitive.Positioner>\n        </TooltipPrimitive.Portal>\n    );\n}\n\nexport { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };\n"
    }
  ],
  "mcp": {
    "description": "A popup that displays information when hovering or focusing an element. Provides shared provider layout glide, dimension morphing, and blur-replaced content transitions.",
    "tags": [
      "component",
      "tooltip",
      "paceui",
      "animation",
      "popover",
      "hint"
    ]
  }
}