{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"tooltip","type":"registry:component","title":"Tooltip","description":"Hover or focus tooltip that fades in beside its trigger. After the first one opens, nearby tooltips appear without a delay.","author":"Ryan","dependencies":["clsx","motion","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/motion/tooltip.tsx","type":"registry:component","target":"@components/motion/tooltip.tsx","content":"\"use client\";\n// easeui.dev/components/motion/tooltip\n\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport {\n  cloneElement,\n  isValidElement,\n  type PointerEvent,\n  type ReactElement,\n  type ReactNode,\n  useCallback,\n  useEffect,\n  useId,\n  useRef,\n  useState,\n} from \"react\";\nimport { cn } from \"@/lib/utils\";\n\ntype Side = \"top\" | \"bottom\" | \"left\" | \"right\";\n\nexport interface TooltipProps {\n  content: ReactNode;\n  /** The element the tooltip describes. */\n  children: ReactNode;\n  /** Which side of the trigger the tooltip appears on. Default \"top\". */\n  side?: Side;\n  /** Milliseconds to wait before the first tooltip opens. Default 200. */\n  delay?: number;\n  className?: string;\n  wrapperClassName?: string;\n}\n\nconst EASE = [0.23, 1, 0.32, 1] as const;\n\n// After one tooltip closes, others open at once with no animation for a short\n// window, so moving along a toolbar feels instant.\nconst WARM_MS = 400;\nlet lastClosedAt = 0;\n\nconst POSITION: Record<Side, string> = {\n  top: \"bottom-full left-1/2 mb-2 -translate-x-1/2\",\n  bottom: \"top-full left-1/2 mt-2 -translate-x-1/2\",\n  left: \"right-full top-1/2 mr-2 -translate-y-1/2\",\n  right: \"left-full top-1/2 ml-2 -translate-y-1/2\",\n};\n\nconst ORIGIN: Record<Side, string> = {\n  top: \"bottom center\",\n  bottom: \"top center\",\n  left: \"center right\",\n  right: \"center left\",\n};\n\nexport function Tooltip({\n  content,\n  children,\n  side = \"top\",\n  delay = 200,\n  className,\n  wrapperClassName,\n}: TooltipProps) {\n  const [open, setOpen] = useState(false);\n  const [instant, setInstant] = useState(false);\n  const id = useId();\n  const reduce = useReducedMotion();\n  const timer = useRef<ReturnType<typeof setTimeout> | null>(null);\n  const openRef = useRef(false);\n  openRef.current = open;\n\n  const clear = useCallback(() => {\n    if (timer.current) clearTimeout(timer.current);\n    timer.current = null;\n  }, []);\n\n  const show = useCallback(() => {\n    clear();\n    if (Date.now() - lastClosedAt < WARM_MS) {\n      setInstant(true);\n      setOpen(true);\n      return;\n    }\n    setInstant(false);\n    timer.current = setTimeout(() => setOpen(true), delay);\n  }, [clear, delay]);\n\n  const hide = useCallback(() => {\n    clear();\n    if (openRef.current) lastClosedAt = Date.now();\n    setOpen(false);\n  }, [clear]);\n\n  useEffect(() => clear, [clear]);\n\n  useEffect(() => {\n    if (!open) return;\n    const onKeyDown = (event: KeyboardEvent) => {\n      if (event.key === \"Escape\") hide();\n    };\n    window.addEventListener(\"keydown\", onKeyDown);\n    return () => window.removeEventListener(\"keydown\", onKeyDown);\n  }, [open, hide]);\n\n  const trigger = isValidElement(children)\n    ? cloneElement(children as ReactElement<Record<string, unknown>>, {\n        \"aria-describedby\": open ? id : undefined,\n      })\n    : children;\n\n  const skipMotion = reduce || instant;\n\n  return (\n    // biome-ignore lint/a11y/noStaticElementInteractions: the wrapper only observes hover and focus on the trigger it contains.\n    <span\n      className={cn(\"relative inline-flex\", wrapperClassName)}\n      // Hover opens only for a real mouse, so a tap on a phone never sticks a tooltip open.\n      onPointerEnter={(event: PointerEvent) => {\n        if (event.pointerType === \"mouse\") show();\n      }}\n      onPointerLeave={hide}\n      onFocus={show}\n      onBlur={hide}\n    >\n      {trigger}\n      <AnimatePresence>\n        {open ? (\n          <motion.span\n            role=\"tooltip\"\n            id={id}\n            initial={skipMotion ? { opacity: 1, scale: 1 } : { opacity: 0, scale: 0.97 }}\n            animate={{ opacity: 1, scale: 1 }}\n            exit={{ opacity: 0, transition: { duration: reduce ? 0 : 0.08, ease: EASE } }}\n            transition={{ duration: skipMotion ? 0 : 0.12, ease: EASE }}\n            style={{ transformOrigin: ORIGIN[side] }}\n            className={cn(\n              \"pointer-events-none absolute z-50 whitespace-nowrap rounded-md bg-foreground px-2 py-1 text-xs font-medium text-background shadow-md\",\n              POSITION[side],\n              className,\n            )}\n          >\n            {content}\n          </motion.span>\n        ) : null}\n      </AnimatePresence>\n    </span>\n  );\n}\n"},{"path":"lib/utils.ts","type":"registry:lib","target":"@lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"}]}