{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tooltip",
  "type": "registry:ui",
  "title": "Tooltip",
  "description": "An accessible React tooltip for Next.js and Tailwind CSS: a short hint on hover and keyboard focus, fast across a toolbar, built on Radix Tooltip.",
  "author": "FewTone <https://fewtone.com>",
  "dependencies": [
    "radix-ui@^1.7.0"
  ],
  "registryDependencies": [
    "https://ui.fewtone.com/r/tokens.json"
  ],
  "files": [
    {
      "path": "registry/fewtone/ui/tooltip.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { Tooltip as TooltipPrimitive } from \"radix-ui\";\nimport { type ComponentProps, createContext, useContext, useEffect, useMemo, useRef, useState } from \"react\";\n\n/*\n * Tooltip, built on Radix Tooltip: a short hint that shows on hover and on keyboard focus, and names nothing\n * itself (an icon button still needs its own aria-label; the tooltip describes it). Escape closes it.\n * - The first tooltip waits a moment, then opens out of its trigger through the blur (the popover-in\n *   keyframes, scaling from Radix's transform origin).\n * - Moving along a row of triggers (a toolbar), the next tooltips appear at once and take the last one's\n *   place: no delay and no animation, and the one before goes in the same frame instead of fading out\n *   under the new one. Keyboard focus works the same way. Leaving the row, the last tooltip closes back\n *   into its trigger; after `skipDelayDuration` the next one waits and animates again.\n * - The provider stays \"warm\" while a tooltip shows and for the skip window after, and its tooltips skip the\n *   delay meanwhile. Radix's own skip window starts when the previous tooltip hands over, so resting on a\n *   tooltip for longer than the window would bring the delay back for the next one.\n * - Nothing animates on first paint.\n * - A Tooltip outside a TooltipProvider gets its own, so one on its own just works. Wrap a toolbar (or the\n *   app) in one TooltipProvider so its tooltips share the delay and the hand-over.\n */\n\n/** The tooltips under one provider hand over to each other through this */\ntype Group = {\n  /** A tooltip opens: whether it takes the place of one that's showing or closed a moment ago */\n  open: (tooltip: object) => boolean;\n  /** A tooltip closes; `hide` makes it vanish at once if the next one takes its place */\n  close: (tooltip: object, hide: () => void) => void;\n};\n\nconst GroupContext = createContext<Group | null>(null);\n\n/** A tooltip is showing, or closed less than the skip window ago: the next one opens without the delay */\nconst WarmContext = createContext(false);\n\n/** One tooltip's way of coming and going, read by its content */\nconst TooltipContext = createContext<{ following: boolean; hidden: boolean } | null>(null);\n\nfunction TooltipProvider({\n  delayDuration = 300,\n  skipDelayDuration = 300,\n  ...props\n}: ComponentProps<typeof TooltipPrimitive.Provider>) {\n  const [warm, setWarm] = useState(false);\n  const record = useRef({\n    showing: null as object | null,\n    closedAt: Number.NEGATIVE_INFINITY,\n    hideLast: null as (() => void) | null,\n    cooling: 0,\n  });\n\n  useEffect(() => () => window.clearTimeout(record.current.cooling), []);\n\n  const group = useMemo<Group>(\n    () => ({\n      open: (tooltip) => {\n        const last = record.current;\n        const follows = last.showing !== null || performance.now() - last.closedAt < skipDelayDuration;\n        // Radix closes the one before just ahead of this one opening; it goes in this frame\n        if (follows) last.hideLast?.();\n        last.showing = tooltip;\n        window.clearTimeout(last.cooling);\n        setWarm(true);\n        return follows;\n      },\n      close: (tooltip, hide) => {\n        const last = record.current;\n        if (last.showing !== tooltip) return;\n        last.showing = null;\n        last.closedAt = performance.now();\n        last.hideLast = hide;\n        last.cooling = window.setTimeout(() => setWarm(false), skipDelayDuration);\n      },\n    }),\n    [skipDelayDuration],\n  );\n\n  return (\n    <GroupContext.Provider value={group}>\n      <WarmContext.Provider value={warm}>\n        <TooltipPrimitive.Provider\n          data-slot=\"tooltip-provider\"\n          delayDuration={delayDuration}\n          skipDelayDuration={skipDelayDuration}\n          {...props}\n        />\n      </WarmContext.Provider>\n    </GroupContext.Provider>\n  );\n}\n\ntype TooltipProps = ComponentProps<typeof TooltipPrimitive.Root>;\n\nfunction TooltipRoot({\n  group,\n  open: openProp,\n  defaultOpen = false,\n  onOpenChange,\n  delayDuration,\n  ...props\n}: TooltipProps & { group: Group }) {\n  const warm = useContext(WarmContext);\n  // This tooltip's identity in the group\n  const self = useRef({});\n  const [uncontrolled, setUncontrolled] = useState(defaultOpen);\n  // Open from the start counts as following: it's simply there, with nothing animating on first paint\n  const [following, setFollowing] = useState(defaultOpen);\n  const [hidden, setHidden] = useState(false);\n  const open = openProp ?? uncontrolled;\n\n  // A tooltip that unmounts while showing counts as closed, so the group doesn't stay warm\n  useEffect(() => {\n    const tooltip = self.current;\n    return () => group.close(tooltip, () => {});\n  }, [group]);\n\n  return (\n    <TooltipContext.Provider value={{ following, hidden }}>\n      <TooltipPrimitive.Root\n        data-slot=\"tooltip\"\n        open={open}\n        onOpenChange={(next) => {\n          // Radix's timers can report a state it's already in\n          if (next === open) return;\n          if (next) {\n            setFollowing(group.open(self.current));\n            setHidden(false);\n          } else {\n            group.close(self.current, () => setHidden(true));\n          }\n          if (openProp === undefined) setUncontrolled(next);\n          onOpenChange?.(next);\n        }}\n        delayDuration={warm ? 0 : delayDuration}\n        {...props}\n      />\n    </TooltipContext.Provider>\n  );\n}\n\nfunction Tooltip(props: TooltipProps) {\n  const group = useContext(GroupContext);\n  if (group) return <TooltipRoot group={group} {...props} />;\n  return (\n    <TooltipProvider>\n      <Tooltip {...props} />\n    </TooltipProvider>\n  );\n}\n\nfunction TooltipTrigger(props: ComponentProps<typeof TooltipPrimitive.Trigger>) {\n  return <TooltipPrimitive.Trigger data-slot=\"tooltip-trigger\" {...props} />;\n}\n\nfunction TooltipContent({\n  className,\n  sideOffset = 4,\n  children,\n  ...props\n}: ComponentProps<typeof TooltipPrimitive.Content>) {\n  const tooltip = useContext(TooltipContext);\n\n  return (\n    <TooltipPrimitive.Portal>\n      <TooltipPrimitive.Content\n        data-slot=\"tooltip-content\"\n        data-following={tooltip?.following ? \"\" : undefined}\n        sideOffset={sideOffset}\n        className={cn(\n          \"z-(--z-tooltip) w-fit max-w-xs rounded-md bg-primary px-2 py-1 text-xs text-balance text-primary-foreground\",\n          \"origin-(--radix-tooltip-content-transform-origin) data-[state=closed]:animate-popover-out\",\n          // Radix's open states are delayed-open and instant-open; one that follows another doesn't animate\n          !tooltip?.following &&\n            \"data-[state=delayed-open]:animate-popover-in data-[state=instant-open]:animate-popover-in\",\n          // Handed over to the next tooltip: gone at once, while Radix waits out the closing keyframes\n          tooltip?.hidden && \"invisible\",\n          className,\n        )}\n        {...props}\n      >\n        {children}\n      </TooltipPrimitive.Content>\n    </TooltipPrimitive.Portal>\n  );\n}\n\nexport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };\n"
    }
  ],
  "categories": [
    "overlays"
  ],
  "docs": "Usage and examples: https://ui.fewtone.com/components/tooltip"
}
