{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "tooltip", "title": "Tooltip (Base UI)", "description": "Floating tooltip with spring-based animations, configurable placement, and rich content support. Base UI flavor.", "dependencies": [ "framer-motion", "@base-ui/react" ], "registryDependencies": [ "https://zeron-ui.vercel.app/r/surfaces.json", "utils", "https://zeron-ui.vercel.app/r/springs.json", "https://zeron-ui.vercel.app/r/shape-context.json", "https://zeron-ui.vercel.app/r/portal-container-context.json" ], "files": [ { "path": "src/components/ui/tooltip.tsx", "content": "\"use client\";\n\nimport {\n createContext,\n useContext,\n useState,\n type ReactNode,\n} from \"react\";\nimport { Tooltip as TooltipPrimitive } from \"@base-ui/react/tooltip\";\nimport { motion } from \"framer-motion\";\nimport { cn } from \"@/lib/utils\";\nimport { spring } from \"@/lib/springs\";\nimport { useShape } from \"@/lib/shape-context\";\nimport { usePortalContainer } from \"@/lib/portal-container-context\";\n\n// ---------------------------------------------------------------------------\n// Portal container context\n// ---------------------------------------------------------------------------\n\nconst TooltipPortalContainerContext = createContext(null);\n\nfunction TooltipPortalContainer({\n value,\n children,\n}: {\n value: HTMLElement | null;\n children: ReactNode;\n}) {\n return (\n \n {children}\n \n );\n}\n\n// ---------------------------------------------------------------------------\n// Provider\n// ---------------------------------------------------------------------------\n\nconst DEFAULT_DELAY = 200;\n\n// Tracks whether an app-level is above us. Each Tooltip\n// only wraps itself in a local primitive Provider when there isn't one —\n// a per-instance Provider would defeat cross-tooltip skip-delay grouping\n// (moving between adjacent tooltips would re-wait the full delay).\nconst TooltipGroupContext = createContext(false);\n\ninterface TooltipProviderProps {\n children: ReactNode;\n /** Hover delay before tooltips open, in ms. Defaults to 200. */\n delayDuration?: number;\n /** After a tooltip closes, adjacent tooltips opened within this window\n * skip the hover delay, in ms. Defaults to 300. */\n skipDelayDuration?: number;\n}\n\n/** Groups descendant Tooltips so that once one opens, moving to an adjacent\n * trigger shows its tooltip instantly instead of re-waiting the full delay.\n * Wrap once at the app (or section) level; bare Tooltips still work without\n * it via a per-instance fallback. */\nfunction TooltipProvider({\n children,\n delayDuration = DEFAULT_DELAY,\n skipDelayDuration = 300,\n}: TooltipProviderProps) {\n return (\n \n \n {children}\n \n \n );\n}\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\ntype TooltipSide = \"top\" | \"right\" | \"bottom\" | \"left\";\n\ninterface TooltipProps {\n content: ReactNode;\n children: React.ReactElement;\n side?: TooltipSide;\n sideOffset?: number;\n /** Hover delay before this tooltip opens, in ms. Defaults to 200, or to the\n * ambient TooltipProvider's delayDuration when one is present. */\n delayDuration?: number;\n className?: string;\n /** When true, forces the tooltip open. When false, forces it closed. When undefined, uses default hover/focus behavior. */\n forceOpen?: boolean;\n /** Called when the tooltip's internal open state changes (before forceOpen is applied). */\n onOpenChange?: (open: boolean) => void;\n}\n\n// ---------------------------------------------------------------------------\n// Animation helpers\n// ---------------------------------------------------------------------------\n\nfunction getSlideOffset(side: TooltipSide) {\n switch (side) {\n case \"top\":\n return { y: 4 };\n case \"bottom\":\n return { y: -4 };\n case \"left\":\n return { x: 4 };\n case \"right\":\n return { x: -4 };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Tooltip\n// ---------------------------------------------------------------------------\n\nfunction Tooltip({\n content,\n children,\n side = \"top\",\n sideOffset = 8,\n delayDuration,\n className,\n forceOpen,\n onOpenChange: onOpenChangeProp,\n}: TooltipProps) {\n const [internalOpen, setInternalOpen] = useState(false);\n const open = forceOpen !== undefined ? forceOpen : internalOpen;\n const shape = useShape();\n const portalContainer = useContext(TooltipPortalContainerContext);\n const fullscreenPortal = usePortalContainer();\n const hasAmbientProvider = useContext(TooltipGroupContext);\n\n const slideOffset = getSlideOffset(side);\n\n const tooltip = (\n {\n setInternalOpen(v);\n onOpenChangeProp?.(v);\n }}\n >\n {/* An explicit delayDuration overrides the ambient provider's delay;\n left undefined, the trigger inherits it from the provider. */}\n \n \n \n {\n const exiting = state.transitionStatus === \"ending\";\n const {\n style: baseStyle,\n // motion.div has incompatible drag/animation event signatures —\n // strip the React-DOM versions so they don't fight motion's own.\n onDrag: _onDrag,\n onDragStart: _onDragStart,\n onDragEnd: _onDragEnd,\n onAnimationStart: _onAnimationStart,\n onAnimationEnd: _onAnimationEnd,\n onAnimationIteration: _onAnimationIteration,\n ...rest\n } = props as React.HTMLAttributes;\n return (\n \n );\n }}\n >\n {content}\n \n \n \n \n );\n\n // Fallback: without an ambient TooltipProvider, give this instance its own\n // so a bare keeps the library's default delay. Grouped skip-delay\n // needs the shared app-level TooltipProvider.\n if (hasAmbientProvider) return tooltip;\n\n return (\n \n {tooltip}\n \n );\n}\n\nexport { Tooltip, TooltipPortalContainer, TooltipProvider };\nexport type { TooltipProps, TooltipProviderProps, TooltipSide };\n", "type": "registry:ui", "target": "components/ui/tooltip.tsx" } ], "type": "registry:ui" }