{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tooltip",
  "type": "registry:ui",
  "title": "Tooltip",
  "description": "Short supporting text for unfamiliar controls.",
  "categories": [
    "disclosure"
  ],
  "dependencies": [
    "@radix-ui/react-tooltip",
    "motion"
  ],
  "registryDependencies": [
    "https://irisui.dev/r/foundation.json",
    "https://irisui.dev/r/motion-tokens.json"
  ],
  "files": [
    {
      "path": "src/registry/components/tooltip/tooltip.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore } from \"react\";\nimport type { ReactElement, ReactNode } from \"react\";\nimport * as TooltipPrimitive from \"@radix-ui/react-tooltip\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { motionTokens } from \"../lib/motion-tokens\";\n\nexport interface TooltipProps {\n  content: ReactNode;\n  children: ReactElement;\n  side?: \"top\" | \"bottom\" | \"left\" | \"right\";\n}\n\nconst DELAY = 250;\nconst SKIP_WINDOW = 300;\n\n/* Every Tooltip brings its own provider, so the skip window is shared here: while any tooltip is open, and briefly after the last one closes, the next opens without delay or travel. */\nlet warm = false;\nlet openCount = 0;\nlet coolTimer = 0;\nconst listeners = new Set<() => void>();\nconst warmth = {\n  subscribe: (listener: () => void) => {\n    listeners.add(listener);\n    return () => {\n      listeners.delete(listener);\n    };\n  },\n  get: () => warm,\n  set: (next: boolean) => {\n    if (warm === next) return;\n    warm = next;\n    listeners.forEach((listener) => listener());\n  },\n  opened: () => {\n    openCount += 1;\n    window.clearTimeout(coolTimer);\n    warmth.set(true);\n  },\n  closed: () => {\n    openCount = Math.max(0, openCount - 1);\n    if (openCount) return;\n    window.clearTimeout(coolTimer);\n    coolTimer = window.setTimeout(() => warmth.set(false), SKIP_WINDOW);\n  },\n};\n\n/** String content crossfades when it changes while open, and the bubble springs to the new text size. */\nfunction TooltipText({ text }: { text: string }) {\n  const reduced = useReducedMotion();\n  const measure = useRef<HTMLSpanElement>(null);\n  const measured = useRef<string | null>(null);\n  const [size, setSize] = useState<{ width: number; height: number; animate: boolean } | null>(null);\n  useLayoutEffect(() => {\n    const node = measure.current;\n    if (!node) return;\n    const observer = new ResizeObserver(([entry]) => {\n      if (!entry) return;\n      const box = entry.borderBoxSize?.[0];\n      const current = node.textContent;\n      const animate = measured.current !== null && measured.current !== current;\n      measured.current = current;\n      setSize({\n        width: Math.ceil(box?.inlineSize ?? node.offsetWidth),\n        height: Math.ceil(box?.blockSize ?? node.offsetHeight),\n        animate,\n      });\n    });\n    observer.observe(node);\n    return () => observer.disconnect();\n  }, []);\n  return (\n    <motion.span\n      className=\"relative block overflow-clip [overflow-clip-margin:var(--space-2)]\"\n      initial={false}\n      animate={size ? { width: size.width, height: size.height } : undefined}\n      transition={size?.animate && !reduced ? motionTokens.spring.morph : { duration: 0 }}\n    >\n      <span\n        ref={measure}\n        className=\"pointer-events-none invisible absolute top-0 left-0 w-max max-w-[calc(15rem_-_2_*_var(--space-4)_-_2px)]\"\n        aria-hidden=\"true\"\n      >\n        {text}\n      </span>\n      <AnimatePresence mode=\"popLayout\" initial={false}>\n        <motion.span\n          key={text}\n          className=\"block w-max max-w-[calc(15rem_-_2_*_var(--space-4)_-_2px)]\"\n          initial={reduced ? false : { opacity: 0, y: \"0.3em\", filter: `blur(${motionTokens.blur.soft}px)` }}\n          animate={{ opacity: 1, y: 0, filter: \"blur(0px)\" }}\n          exit={\n            reduced\n              ? { opacity: 0, transition: { duration: 0 } }\n              : {\n                  opacity: 0,\n                  y: \"-0.3em\",\n                  filter: `blur(${motionTokens.blur.subtle}px)`,\n                  transition: { duration: motionTokens.duration.instant, ease: [...motionTokens.ease.standard] },\n                }\n          }\n          transition={{ duration: motionTokens.duration.standard, ease: [...motionTokens.ease.enter] }}\n        >\n          {text}\n        </motion.span>\n      </AnimatePresence>\n    </motion.span>\n  );\n}\n\n/* The first tooltip waits, then rises a few pixels from its trigger. Within the skip window the next one only fades. Transitions instead of\n   keyframes, so returning to the trigger while it fades out reverses the fade instead of restarting it. Radix unmounts when the exit\n   animation ends, so a no-op keyframe times the exit while the transition does the visual work. */\nconst bubble = [\n  \"z-90 max-w-60 origin-(--radix-tooltip-content-transform-origin) rounded-control border border-[color-mix(in_oklab,var(--page)_14%,var(--ink))] bg-ink px-4 py-3 text-sm leading-body font-normal text-page shadow-raised [--tooltip-y:3px] [transition:opacity_var(--duration-fast)_var(--ease-enter),transform_var(--duration-fast)_var(--ease-enter)] data-[side=bottom]:[--tooltip-y:-3px]\",\n  \"starting:[&[data-state$=-open]]:[transform:translateY(var(--tooltip-y))_scale(.97)] starting:[&[data-state$=-open]]:opacity-0 starting:[&[data-instant][data-state$=-open]]:transform-none\",\n  \"[&[data-instant][data-state$=-open]]:[transition:opacity_var(--duration-instant)_var(--ease-standard),transform_var(--duration-instant)_var(--ease-standard)]\",\n  \"data-[state=closed]:animate-tooltip-exit data-[state=closed]:opacity-0 data-[state=closed]:[transform:scale(.98)] data-[state=closed]:[transition:opacity_var(--duration-instant)_var(--ease-standard),transform_var(--duration-instant)_var(--ease-standard)]\",\n  \"motion-reduce:transform-none motion-reduce:[transition:opacity_var(--duration-instant)_var(--ease-standard)] motion-reduce:data-[state=closed]:transform-none motion-reduce:data-[state=closed]:[transition:opacity_var(--duration-instant)_var(--ease-standard)] motion-reduce:[&[data-instant][data-state$=-open]]:transform-none motion-reduce:[&[data-instant][data-state$=-open]]:[transition:opacity_var(--duration-instant)_var(--ease-standard)]\",\n].join(\" \");\n\nexport function Tooltip({ content, children, side = \"top\" }: TooltipProps) {\n  const isWarm = useSyncExternalStore(warmth.subscribe, warmth.get, () => false);\n  // Controlled so the instant flag lands in the same render that mounts the content (Radix reports uncontrolled changes a frame late).\n  const [open, setOpen] = useState(false);\n  const [instant, setInstant] = useState(false);\n  useEffect(() => {\n    if (!open) return;\n    warmth.opened();\n    return warmth.closed;\n  }, [open]);\n  return (\n    <TooltipPrimitive.Provider delayDuration={DELAY} skipDelayDuration={0}>\n      <TooltipPrimitive.Root\n        open={open}\n        delayDuration={isWarm ? 0 : DELAY}\n        onOpenChange={(next) => {\n          if (next) setInstant(warmth.get());\n          setOpen(next);\n        }}\n      >\n        <TooltipPrimitive.Trigger asChild>{children}</TooltipPrimitive.Trigger>\n        <TooltipPrimitive.Portal>\n          <TooltipPrimitive.Content\n            className={bubble}\n            data-instant={instant || undefined}\n            side={side}\n            sideOffset={8}\n            collisionPadding={12}\n          >\n            {typeof content === \"string\" || typeof content === \"number\" ? (\n              <TooltipText text={String(content)} />\n            ) : (\n              content\n            )}\n          </TooltipPrimitive.Content>\n        </TooltipPrimitive.Portal>\n      </TooltipPrimitive.Root>\n    </TooltipPrimitive.Provider>\n  );\n}\n\nexport default Tooltip;\n",
      "type": "registry:component",
      "target": "@components/iris/tooltip/tooltip.tsx"
    }
  ],
  "docs": "Docs and live preview: https://irisui.dev/components/tooltip",
  "meta": {
    "kind": "component",
    "group": "Overlays & disclosure",
    "export": "Tooltip",
    "tags": [
      "hint",
      "overlay"
    ],
    "docs": "https://irisui.dev/components/tooltip"
  }
}
