{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tooltip",
  "type": "registry:ui",
  "title": "Tooltip",
  "description": "An animated tooltip that shows contextual info on hover or focus and smoothly glides to the next element without disappearing between transitions.",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "registry/components/tooltip/index.tsx",
      "content": "'use client';\n\nimport * as React from 'react';\nimport { createPortal } from 'react-dom';\nimport {\n  motion,\n  AnimatePresence,\n  LayoutGroup,\n  type Transition,\n} from 'motion/react';\n\nimport { cn } from '@/lib/utils';\n\ntype Side = 'top' | 'bottom' | 'left' | 'right';\n\ntype Align = 'start' | 'center' | 'end';\n\ntype TooltipData = {\n  content: React.ReactNode;\n  rect: DOMRect;\n  side: Side;\n  sideOffset: number;\n  align: Align;\n  alignOffset: number;\n  id: string;\n  arrow: boolean;\n};\n\ntype GlobalTooltipContextType = {\n  showTooltip: (data: TooltipData) => void;\n  hideTooltip: () => void;\n  currentTooltip: TooltipData | null;\n  transition: Transition;\n  globalId: string;\n};\n\nconst GlobalTooltipContext = React.createContext<\n  GlobalTooltipContextType | undefined\n>(undefined);\n\nconst useGlobalTooltip = () => {\n  const context = React.useContext(GlobalTooltipContext);\n  if (!context) {\n    throw new Error('useGlobalTooltip must be used within a TooltipProvider');\n  }\n  return context;\n};\n\ntype TooltipPosition = {\n  x: number;\n  y: number;\n  transform: string;\n  initial: { x?: number; y?: number };\n};\n\nfunction getTooltipPosition({\n  rect,\n  side,\n  sideOffset,\n  align,\n  alignOffset,\n}: {\n  rect: DOMRect;\n  side: Side;\n  sideOffset: number;\n  align: Align;\n  alignOffset: number;\n}): TooltipPosition {\n  switch (side) {\n    case 'top':\n      if (align === 'start') {\n        return {\n          x: rect.left + alignOffset,\n          y: rect.top - sideOffset,\n          transform: 'translate(0, -100%)',\n          initial: { y: 15 },\n        };\n      } else if (align === 'end') {\n        return {\n          x: rect.right + alignOffset,\n          y: rect.top - sideOffset,\n          transform: 'translate(-100%, -100%)',\n          initial: { y: 15 },\n        };\n      } else {\n        // center\n        return {\n          x: rect.left + rect.width / 2,\n          y: rect.top - sideOffset,\n          transform: 'translate(-50%, -100%)',\n          initial: { y: 15 },\n        };\n      }\n    case 'bottom':\n      if (align === 'start') {\n        return {\n          x: rect.left + alignOffset,\n          y: rect.bottom + sideOffset,\n          transform: 'translate(0, 0)',\n          initial: { y: -15 },\n        };\n      } else if (align === 'end') {\n        return {\n          x: rect.right + alignOffset,\n          y: rect.bottom + sideOffset,\n          transform: 'translate(-100%, 0)',\n          initial: { y: -15 },\n        };\n      } else {\n        // center\n        return {\n          x: rect.left + rect.width / 2,\n          y: rect.bottom + sideOffset,\n          transform: 'translate(-50%, 0)',\n          initial: { y: -15 },\n        };\n      }\n    case 'left':\n      if (align === 'start') {\n        return {\n          x: rect.left - sideOffset,\n          y: rect.top + alignOffset,\n          transform: 'translate(-100%, 0)',\n          initial: { x: 15 },\n        };\n      } else if (align === 'end') {\n        return {\n          x: rect.left - sideOffset,\n          y: rect.bottom + alignOffset,\n          transform: 'translate(-100%, -100%)',\n          initial: { x: 15 },\n        };\n      } else {\n        // center\n        return {\n          x: rect.left - sideOffset,\n          y: rect.top + rect.height / 2,\n          transform: 'translate(-100%, -50%)',\n          initial: { x: 15 },\n        };\n      }\n    case 'right':\n      if (align === 'start') {\n        return {\n          x: rect.right + sideOffset,\n          y: rect.top + alignOffset,\n          transform: 'translate(0, 0)',\n          initial: { x: -15 },\n        };\n      } else if (align === 'end') {\n        return {\n          x: rect.right + sideOffset,\n          y: rect.bottom + alignOffset,\n          transform: 'translate(0, -100%)',\n          initial: { x: -15 },\n        };\n      } else {\n        // center\n        return {\n          x: rect.right + sideOffset,\n          y: rect.top + rect.height / 2,\n          transform: 'translate(0, -50%)',\n          initial: { x: -15 },\n        };\n      }\n  }\n}\n\ntype TooltipProviderProps = {\n  children: React.ReactNode;\n  openDelay?: number;\n  closeDelay?: number;\n  transition?: Transition;\n};\n\nfunction TooltipProvider({\n  children,\n  openDelay = 700,\n  closeDelay = 300,\n  transition = { type: 'spring', stiffness: 300, damping: 25 },\n}: TooltipProviderProps) {\n  const globalId = React.useId();\n  const [currentTooltip, setCurrentTooltip] =\n    React.useState<TooltipData | null>(null);\n  const timeoutRef = React.useRef<number>(null);\n  const lastCloseTimeRef = React.useRef<number>(0);\n\n  const showTooltip = React.useCallback(\n    (data: TooltipData) => {\n      if (timeoutRef.current) clearTimeout(timeoutRef.current);\n      if (currentTooltip !== null) {\n        setCurrentTooltip(data);\n        return;\n      }\n      const now = Date.now();\n      const delay = now - lastCloseTimeRef.current < closeDelay ? 0 : openDelay;\n      timeoutRef.current = window.setTimeout(\n        () => setCurrentTooltip(data),\n        delay,\n      );\n    },\n    [openDelay, closeDelay, currentTooltip],\n  );\n\n  const hideTooltip = React.useCallback(() => {\n    if (timeoutRef.current) clearTimeout(timeoutRef.current);\n    timeoutRef.current = window.setTimeout(() => {\n      setCurrentTooltip(null);\n      lastCloseTimeRef.current = Date.now();\n    }, closeDelay);\n  }, [closeDelay]);\n\n  const hideImmediate = React.useCallback(() => {\n    if (timeoutRef.current) clearTimeout(timeoutRef.current);\n    setCurrentTooltip(null);\n    lastCloseTimeRef.current = Date.now();\n  }, []);\n\n  React.useEffect(() => {\n    window.addEventListener('scroll', hideImmediate, true);\n    return () => window.removeEventListener('scroll', hideImmediate, true);\n  }, [hideImmediate]);\n\n  return (\n    <GlobalTooltipContext.Provider\n      value={{\n        showTooltip,\n        hideTooltip,\n        currentTooltip,\n        transition,\n        globalId,\n      }}\n    >\n      <LayoutGroup>{children}</LayoutGroup>\n      <TooltipOverlay />\n    </GlobalTooltipContext.Provider>\n  );\n}\n\ntype TooltipArrowProps = {\n  side: Side;\n};\n\nfunction TooltipArrow({ side }: TooltipArrowProps) {\n  return (\n    <div\n      className={cn(\n        'absolute bg-primary z-50 size-2.5 rotate-45 rounded-[2px]',\n        (side === 'top' || side === 'bottom') && 'left-1/2 -translate-x-1/2',\n        (side === 'left' || side === 'right') && 'top-1/2 -translate-y-1/2',\n        side === 'top' && '-bottom-[3px]',\n        side === 'bottom' && '-top-[3px]',\n        side === 'left' && '-right-[3px]',\n        side === 'right' && '-left-[3px]',\n      )}\n    />\n  );\n}\n\ntype TooltipPortalProps = {\n  children: React.ReactNode;\n};\n\nfunction TooltipPortal({ children }: TooltipPortalProps) {\n  const [isMounted, setIsMounted] = React.useState(false);\n  React.useEffect(() => setIsMounted(true), []);\n  return isMounted ? createPortal(children, document.body) : null;\n}\n\nfunction TooltipOverlay() {\n  const { currentTooltip, transition, globalId } = useGlobalTooltip();\n\n  const position = React.useMemo(() => {\n    if (!currentTooltip) return null;\n    return getTooltipPosition({\n      rect: currentTooltip.rect,\n      side: currentTooltip.side,\n      sideOffset: currentTooltip.sideOffset,\n      align: currentTooltip.align,\n      alignOffset: currentTooltip.alignOffset,\n    });\n  }, [currentTooltip]);\n\n  return (\n    <AnimatePresence>\n      {currentTooltip && currentTooltip.content && position && (\n        <TooltipPortal>\n          <motion.div\n            data-slot=\"tooltip-overlay-container\"\n            className=\"fixed z-50\"\n            style={{\n              top: position.y,\n              left: position.x,\n              transform: position.transform,\n            }}\n          >\n            <motion.div\n              data-slot=\"tooltip-overlay\"\n              layoutId={`tooltip-overlay-${globalId}`}\n              initial={{ opacity: 0, scale: 0, ...position.initial }}\n              animate={{ opacity: 1, scale: 1, x: 0, y: 0 }}\n              exit={{ opacity: 0, scale: 0, ...position.initial }}\n              transition={transition}\n              className=\"relative rounded-md bg-primary fill-primary px-3 py-1.5 text-sm text-primary-foreground shadow-md w-fit text-balance\"\n            >\n              {currentTooltip.content}\n\n              {currentTooltip.arrow && (\n                <TooltipArrow side={currentTooltip.side} />\n              )}\n            </motion.div>\n          </motion.div>\n        </TooltipPortal>\n      )}\n    </AnimatePresence>\n  );\n}\n\ntype TooltipContextType = {\n  content: React.ReactNode;\n  setContent: React.Dispatch<React.SetStateAction<React.ReactNode>>;\n  arrow: boolean;\n  setArrow: React.Dispatch<React.SetStateAction<boolean>>;\n  side: Side;\n  sideOffset: number;\n  align: Align;\n  alignOffset: number;\n  id: string;\n};\n\nconst TooltipContext = React.createContext<TooltipContextType | undefined>(\n  undefined,\n);\n\nconst useTooltip = () => {\n  const context = React.useContext(TooltipContext);\n  if (!context) {\n    throw new Error('useTooltip must be used within a TooltipProvider');\n  }\n  return context;\n};\n\ntype TooltipProps = {\n  children: React.ReactNode;\n  side?: Side;\n  sideOffset?: number;\n  align?: Align;\n  alignOffset?: number;\n};\n\nfunction Tooltip({\n  children,\n  side = 'top',\n  sideOffset = 14,\n  align = 'center',\n  alignOffset = 0,\n}: TooltipProps) {\n  const id = React.useId();\n  const [content, setContent] = React.useState<React.ReactNode>(null);\n  const [arrow, setArrow] = React.useState(true);\n\n  return (\n    <TooltipContext.Provider\n      value={{\n        content,\n        setContent,\n        arrow,\n        setArrow,\n        side,\n        sideOffset,\n        align,\n        alignOffset,\n        id,\n      }}\n    >\n      {children}\n    </TooltipContext.Provider>\n  );\n}\n\ntype TooltipContentProps = {\n  children: React.ReactNode;\n  arrow?: boolean;\n};\n\nfunction TooltipContent({ children, arrow = true }: TooltipContentProps) {\n  const { setContent, setArrow } = useTooltip();\n  React.useEffect(() => {\n    setContent(children);\n    setArrow(arrow);\n  }, [children, setContent, setArrow, arrow]);\n  return null;\n}\n\ntype TooltipTriggerProps = {\n  children: React.ReactElement;\n};\n\nfunction TooltipTrigger({ children }: TooltipTriggerProps) {\n  const { content, side, sideOffset, align, alignOffset, id, arrow } =\n    useTooltip();\n  const { showTooltip, hideTooltip, currentTooltip } = useGlobalTooltip();\n  const triggerRef = React.useRef<HTMLElement>(null);\n\n  const handleOpen = React.useCallback(() => {\n    if (!triggerRef.current) return;\n    const rect = triggerRef.current.getBoundingClientRect();\n    showTooltip({\n      content,\n      rect,\n      side,\n      sideOffset,\n      align,\n      alignOffset,\n      id,\n      arrow,\n    });\n  }, [showTooltip, content, side, sideOffset, align, alignOffset, id, arrow]);\n\n  const handleMouseEnter = React.useCallback(\n    (e: React.MouseEvent<HTMLElement>) => {\n      (children.props as React.HTMLAttributes<HTMLElement>)?.onMouseEnter?.(e);\n      handleOpen();\n    },\n    [handleOpen, children.props],\n  );\n\n  const handleMouseLeave = React.useCallback(\n    (e: React.MouseEvent<HTMLElement>) => {\n      (children.props as React.HTMLAttributes<HTMLElement>)?.onMouseLeave?.(e);\n      hideTooltip();\n    },\n    [hideTooltip, children.props],\n  );\n\n  const handleFocus = React.useCallback(\n    (e: React.FocusEvent<HTMLElement>) => {\n      (children.props as React.HTMLAttributes<HTMLElement>)?.onFocus?.(e);\n      handleOpen();\n    },\n    [handleOpen, children.props],\n  );\n\n  const handleBlur = React.useCallback(\n    (e: React.FocusEvent<HTMLElement>) => {\n      (children.props as React.HTMLAttributes<HTMLElement>)?.onBlur?.(e);\n      hideTooltip();\n    },\n    [hideTooltip, children.props],\n  );\n\n  React.useEffect(() => {\n    if (currentTooltip?.id !== id) return;\n    if (!triggerRef.current) return;\n\n    if (currentTooltip.content === content && currentTooltip.arrow === arrow)\n      return;\n\n    const rect = triggerRef.current.getBoundingClientRect();\n    showTooltip({\n      content,\n      rect,\n      side,\n      sideOffset,\n      align,\n      alignOffset,\n      id,\n      arrow,\n    });\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [content, arrow, currentTooltip?.id]);\n\n  return React.cloneElement(children, {\n    ref: triggerRef,\n    onMouseEnter: handleMouseEnter,\n    onMouseLeave: handleMouseLeave,\n    onFocus: handleFocus,\n    onBlur: handleBlur,\n    'data-state': currentTooltip?.id === id ? 'open' : 'closed',\n    'data-side': side,\n    'data-align': align,\n    'data-slot': 'tooltip-trigger',\n  } as React.HTMLAttributes<HTMLElement>);\n}\n\nexport {\n  TooltipProvider,\n  Tooltip,\n  TooltipContent,\n  TooltipTrigger,\n  useGlobalTooltip,\n  useTooltip,\n  type TooltipProviderProps,\n  type TooltipProps,\n  type TooltipContentProps,\n  type TooltipTriggerProps,\n};\n",
      "type": "registry:ui",
      "target": "components/animate-ui/components/tooltip.tsx"
    }
  ]
}