{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tooltip",
  "type": "registry:ui",
  "title": "Tooltip",
  "description": "Waits once, then answers neighbors instantly, or glides one tooltip along a toolbar.",
  "categories": [
    "menus"
  ],
  "dependencies": [
    "@base-ui/react"
  ],
  "registryDependencies": [
    "https://www.stealth.pm/r/tokens.json",
    "https://www.stealth.pm/r/cn.json"
  ],
  "files": [
    {
      "path": "components/ui/tooltip.tsx",
      "content": "\"use client\";\nimport { Tooltip as BaseTooltip } from \"@base-ui/react/tooltip\";\nimport { createContext, useContext, useState, useSyncExternalStore } from \"react\";\nimport { cn } from \"@/lib/cn\";\n\ntype Side = \"top\" | \"bottom\" | \"left\" | \"right\";\ntype Align = \"start\" | \"center\" | \"end\";\n\n/* -------------------------------------------------------------------------------------------------\n * Shortcut keys\n * -----------------------------------------------------------------------------------------------*/\n\n// \"mod\" is the platform's command key: ⌘ on Apple devices, Ctrl everywhere else.\n// Tooltips only render after a hover, so reading the platform here never races hydration.\nconst subscribe = () => () => {};\nconst isApple = () => /Mac|iPhone|iPad|iPod/.test(navigator.platform || navigator.userAgent);\nfunction useApple() {\n  return useSyncExternalStore(subscribe, isApple, () => true);\n}\n\nconst glyphs: Record<string, [apple: string, other: string, aria: string]> = {\n  mod: [\"⌘\", \"Ctrl\", \"Meta\"],\n  \"⌘\": [\"⌘\", \"Ctrl\", \"Meta\"],\n  cmd: [\"⌘\", \"Ctrl\", \"Meta\"],\n  shift: [\"⇧\", \"Shift\", \"Shift\"],\n  \"⇧\": [\"⇧\", \"Shift\", \"Shift\"],\n  alt: [\"⌥\", \"Alt\", \"Alt\"],\n  option: [\"⌥\", \"Alt\", \"Alt\"],\n  \"⌥\": [\"⌥\", \"Alt\", \"Alt\"],\n  ctrl: [\"⌃\", \"Ctrl\", \"Control\"],\n  \"⌃\": [\"⌃\", \"Ctrl\", \"Control\"],\n  enter: [\"↵\", \"Enter\", \"Enter\"],\n  esc: [\"Esc\", \"Esc\", \"Escape\"],\n};\n\nconst glyph = (key: string, apple: boolean) => glyphs[key.toLowerCase()]?.[apple ? 0 : 1] ?? (key.length === 1 ? key.toUpperCase() : key);\n\n/** Keys as `aria-keyshortcuts` wants them: \"Meta+Shift+C\". The platform key maps to Meta, like the spec's examples. */\nexport function toAriaShortcut(keys: string[]) {\n  return keys.map((k) => glyphs[k.toLowerCase()]?.[2] ?? (k.length === 1 ? k.toUpperCase() : k)).join(\"+\");\n}\n\nexport type TooltipShortcutProps = React.ComponentProps<\"kbd\"> & { keys: string[] };\n\n/** The key chips a tooltip shows after its label. Also useful in menus and hints. */\nexport function TooltipShortcut({ keys, className, ...rest }: TooltipShortcutProps) {\n  const apple = useApple();\n  return (\n    <kbd className={cn(\"-mr-0.5 flex shrink-0 items-center gap-0.5\", className)} {...rest}>\n      {keys.map((k, i) => {\n        const g = glyph(k, apple);\n        return (\n          <span\n            key={i}\n            className={cn(\n              \"inline-flex h-[18px] min-w-[18px] items-center justify-center rounded-[5px] border border-line-2 bg-frame px-1 leading-none text-fg-2\",\n              // Modifier symbols are drawn by the sans face at full size; letters use mono so I, l and 1 stay distinct.\n              /^[⌘⇧⌥⌃↵]$/.test(g) ? \"font-sans text-[12px]\" : \"font-mono text-[10.5px]\",\n            )}\n          >\n            {g}\n          </span>\n        );\n      })}\n    </kbd>\n  );\n}\n\n/* -------------------------------------------------------------------------------------------------\n * Provider\n * -----------------------------------------------------------------------------------------------*/\n\nexport type TooltipProviderProps = {\n  /** Milliseconds the first tooltip waits on hover before it opens. */\n  delay?: number;\n  /** Milliseconds a tooltip lingers after the pointer leaves. */\n  closeDelay?: number;\n  /** Once one tooltip has closed, neighbors hovered within this many milliseconds open instantly. */\n  timeout?: number;\n  children?: React.ReactNode;\n};\n\n/**\n * Shares one warm-up across every tooltip inside it: the first waits, the ones next to it\n * open on contact, and the group cools down once the pointer has been away for `timeout`.\n */\nexport function TooltipProvider({ delay = 500, closeDelay = 0, timeout = 400, children }: TooltipProviderProps) {\n  return (\n    <BaseTooltip.Provider delay={delay} closeDelay={closeDelay} timeout={timeout}>\n      {children}\n    </BaseTooltip.Provider>\n  );\n}\n\n/* -------------------------------------------------------------------------------------------------\n * The surface\n * -----------------------------------------------------------------------------------------------*/\n\n// The look, shared by the single tooltip and the gliding group.\nconst look = cn(\n  \"relative rounded-lg border border-line-2 bg-raised text-[12px] leading-4 text-fg shadow-pop outline-none\",\n  \"max-w-[min(18rem,var(--available-width))] text-pretty [overflow-wrap:anywhere]\",\n);\n\n// Grows out of the trigger: the origin is the trigger edge and it closes a 2px gap as it\n// scales in. Leaving is a plain 100ms fade. data-instant is set when a warm group hands\n// off to a neighbor or focus opened it, and those render on the same frame.\nconst enter = cn(\n  \"origin-(--transform-origin) ease-out-expo\",\n  \"data-starting-style:scale-96 data-starting-style:opacity-0\",\n  \"data-[side=top]:data-starting-style:translate-y-0.5 data-[side=bottom]:data-starting-style:-translate-y-0.5\",\n  \"data-[side=left]:data-starting-style:translate-x-0.5 data-[side=right]:data-starting-style:-translate-x-0.5\",\n  \"data-ending-style:opacity-0 data-ending-style:ease-out-quart\",\n  \"data-instant:transition-none\",\n  \"motion-reduce:data-starting-style:scale-100 motion-reduce:data-starting-style:translate-0\",\n);\n\nfunction Arrow({ className }: { className?: string }) {\n  return (\n    <BaseTooltip.Arrow\n      className={cn(\n        \"pointer-events-none flex h-[7px] w-3.5\",\n        \"data-[side=bottom]:-top-1.5 data-[side=top]:-bottom-1.5 data-[side=top]:rotate-180\",\n        \"data-[side=left]:-right-2.5 data-[side=left]:rotate-90 data-[side=right]:-left-2.5 data-[side=right]:-rotate-90\",\n        className,\n      )}\n    >\n      {/* Fill plus stroke, overlapping the border by a pixel so the join has no seam. */}\n      <svg width=\"14\" height=\"7\" viewBox=\"0 0 14 7\" fill=\"none\" aria-hidden className=\"block overflow-visible\">\n        <path d=\"M0 7c2.5 0 4-1 5.2-3.6l.6-1.3c.5-1.1 1.9-1.1 2.4 0l.6 1.3C10 6 11.5 7 14 7z\" className=\"fill-raised\" />\n        <path d=\"M0 7c2.5 0 4-1 5.2-3.6l.6-1.3c.5-1.1 1.9-1.1 2.4 0l.6 1.3C10 6 11.5 7 14 7\" className=\"stroke-line-2\" vectorEffect=\"non-scaling-stroke\" />\n      </svg>\n    </BaseTooltip.Arrow>\n  );\n}\n\n/* -------------------------------------------------------------------------------------------------\n * Compound parts\n * -----------------------------------------------------------------------------------------------*/\n\nexport type TooltipRootProps = BaseTooltip.Root.Props;\n\n/** Owns the open state. Uncontrolled by default; `open` + `onOpenChange` to control it. */\nexport function TooltipRoot(props: TooltipRootProps) {\n  return <BaseTooltip.Root {...props} />;\n}\n\nexport type TooltipTriggerProps = BaseTooltip.Trigger.Props & {\n  /** Keys shown in the tooltip, mirrored onto the trigger as `aria-keyshortcuts`. */\n  shortcut?: string[];\n};\n\n/** Put the behavior on your own control with `render`, or wrap it as the only child of `Tooltip`. */\nexport function TooltipTrigger({ shortcut, ...rest }: TooltipTriggerProps) {\n  return <BaseTooltip.Trigger aria-keyshortcuts={shortcut ? toAriaShortcut(shortcut) : undefined} {...rest} />;\n}\n\nexport type TooltipContentProps = Omit<BaseTooltip.Popup.Props, \"className\"> & {\n  className?: string;\n  side?: Side;\n  align?: Align;\n  /** Gap between trigger and tooltip in px. Defaults to 8, or 10 with the arrow. */\n  sideOffset?: number;\n  alignOffset?: number;\n  shortcut?: string[];\n  arrow?: boolean;\n  /** Where to portal the tooltip. Defaults to document.body. */\n  container?: BaseTooltip.Portal.Props[\"container\"];\n};\n\nexport function TooltipContent({\n  side = \"top\",\n  align = \"center\",\n  sideOffset,\n  alignOffset = 0,\n  shortcut,\n  arrow = false,\n  container,\n  className,\n  children,\n  ...rest\n}: TooltipContentProps) {\n  return (\n    <BaseTooltip.Portal container={container}>\n      <BaseTooltip.Positioner\n        side={side}\n        align={align}\n        sideOffset={sideOffset ?? (arrow ? 10 : 8)}\n        alignOffset={alignOffset}\n        collisionPadding={8}\n        arrowPadding={10}\n        className=\"z-(--z-tooltip)\"\n      >\n        <BaseTooltip.Popup\n          className={cn(\n            look,\n            enter,\n            \"flex min-h-[26px] items-center gap-2 px-2 py-[3px]\",\n            \"transition-[opacity,scale,translate] duration-150 data-ending-style:duration-100\",\n            className,\n          )}\n          {...rest}\n        >\n          {arrow && <Arrow />}\n          <span className=\"min-w-0\">{children}</span>\n          {shortcut && shortcut.length > 0 && <TooltipShortcut keys={shortcut} />}\n        </BaseTooltip.Popup>\n      </BaseTooltip.Positioner>\n    </BaseTooltip.Portal>\n  );\n}\n\n/* -------------------------------------------------------------------------------------------------\n * The one-liner\n * -----------------------------------------------------------------------------------------------*/\n\nexport type TooltipProps = Omit<TooltipRootProps, \"children\"> & {\n  /** What the tooltip says. Keep it to one short line; nothing essential lives here. */\n  content: React.ReactNode;\n  shortcut?: string[];\n  side?: Side;\n  align?: Align;\n  sideOffset?: number;\n  arrow?: boolean;\n  /** Overrides the provider's delay for this trigger. */\n  delay?: number;\n  closeDelay?: number;\n  container?: TooltipContentProps[\"container\"];\n  /** Class for the tooltip surface. */\n  className?: string;\n  /** The trigger: a single element that accepts a ref and event handlers, usually a button. */\n  children: React.ReactElement<Record<string, unknown>>;\n};\n\n/** A tooltip around any single control. Icon-only triggers still need their own aria-label. */\nexport function Tooltip({\n  content,\n  shortcut,\n  side,\n  align,\n  sideOffset,\n  arrow,\n  delay,\n  closeDelay,\n  container,\n  className,\n  children,\n  ...root\n}: TooltipProps) {\n  return (\n    <BaseTooltip.Root {...root}>\n      <TooltipTrigger render={children} delay={delay} closeDelay={closeDelay} shortcut={shortcut} />\n      <TooltipContent side={side} align={align} sideOffset={sideOffset} arrow={arrow} shortcut={shortcut} container={container} className={className}>\n        {content}\n      </TooltipContent>\n    </BaseTooltip.Root>\n  );\n}\n\n/* -------------------------------------------------------------------------------------------------\n * Group: one tooltip that glides between the controls of a toolbar\n * -----------------------------------------------------------------------------------------------*/\n\ntype Payload = { content: React.ReactNode; shortcut?: string[] };\ntype Handle = ReturnType<typeof BaseTooltip.createHandle<Payload>>;\nconst GroupContext = createContext<Handle | null>(null);\n\nexport type TooltipGroupProps = {\n  side?: Side;\n  align?: Align;\n  sideOffset?: number;\n  /** Milliseconds before the first tooltip in the group opens. After that it follows the pointer. */\n  delay?: number;\n  container?: TooltipContentProps[\"container\"];\n  /** Class for the shared tooltip surface. */\n  className?: string;\n  children?: React.ReactNode;\n};\n\n/**\n * Every `TooltipGroupTrigger` inside shares one tooltip. Moving along the toolbar slides it to\n * the next control and resizes it to the new label instead of closing one and opening another.\n */\nexport function TooltipGroup({ side = \"top\", align = \"center\", sideOffset = 8, delay = 500, container, className, children }: TooltipGroupProps) {\n  const [handle] = useState(() => BaseTooltip.createHandle<Payload>());\n  return (\n    <GroupContext.Provider value={handle}>\n      <BaseTooltip.Provider delay={delay}>{children}</BaseTooltip.Provider>\n      <BaseTooltip.Root handle={handle}>\n        {({ payload }) => (\n          <BaseTooltip.Portal container={container}>\n            <BaseTooltip.Positioner\n              side={side}\n              align={align}\n              sideOffset={sideOffset}\n              collisionPadding={8}\n              // The positioner itself travels between triggers. Keyboard hand-offs are instant.\n              className={cn(\n                \"z-(--z-tooltip) h-(--positioner-height) w-(--positioner-width) max-w-(--available-width)\",\n                \"transition-[top,left,right,bottom] duration-220 ease-out-quart data-instant:transition-none\",\n              )}\n            >\n              <BaseTooltip.Popup\n                className={cn(\n                  look,\n                  enter,\n                  // Size follows the label; the text inside is pinned to its own width so it never reflows mid-resize.\n                  \"h-(--popup-height,auto) w-(--popup-width,auto) overflow-clip\",\n                  \"transition-[opacity,scale,translate,width,height] [transition-duration:150ms,150ms,150ms,220ms,220ms]\",\n                  \"data-ending-style:[transition-duration:100ms]\",\n                  className,\n                )}\n              >\n                <BaseTooltip.Viewport\n                  className={cn(\n                    \"relative h-full w-full [--pad:8px]\",\n                    \"[&>*]:flex [&>*]:min-h-6 [&>*]:w-max [&>*]:max-w-[min(calc(18rem-2px),calc(var(--available-width)-2px))] [&>*]:items-center [&>*]:gap-2 [&>*]:px-(--pad) [&>*]:py-[3px]\",\n                    \"[&>[data-previous]]:absolute [&>[data-previous]]:left-0 [&>[data-previous]]:top-0\",\n                    \"[&>*]:transition-[opacity,translate,filter] [&>*]:duration-200 [&>*]:ease-out-expo\",\n                    // New label slides in from the side the pointer came from; the old one leaves the other way.\n                    \"data-[activation-direction~=right]:[&>[data-current][data-starting-style]]:translate-x-2\",\n                    \"data-[activation-direction~=left]:[&>[data-current][data-starting-style]]:-translate-x-2\",\n                    \"data-[activation-direction~=down]:[&>[data-current][data-starting-style]]:translate-y-1.5\",\n                    \"data-[activation-direction~=up]:[&>[data-current][data-starting-style]]:-translate-y-1.5\",\n                    \"[&>[data-current][data-starting-style]]:opacity-0 [&>[data-current][data-starting-style]]:blur-[2px]\",\n                    \"data-[activation-direction~=right]:[&>[data-previous][data-ending-style]]:-translate-x-2\",\n                    \"data-[activation-direction~=left]:[&>[data-previous][data-ending-style]]:translate-x-2\",\n                    \"data-[activation-direction~=down]:[&>[data-previous][data-ending-style]]:-translate-y-1.5\",\n                    \"data-[activation-direction~=up]:[&>[data-previous][data-ending-style]]:translate-y-1.5\",\n                    \"[&>[data-previous][data-ending-style]]:opacity-0 [&>[data-previous][data-ending-style]]:blur-[2px] [&>[data-previous]]:duration-120\",\n                    \"data-instant:[&>*]:transition-none motion-reduce:[&>*]:translate-0 motion-reduce:[&>*]:blur-none\",\n                  )}\n                >\n                  {payload && (\n                    <>\n                      <span className=\"min-w-0\">{payload.content}</span>\n                      {payload.shortcut && payload.shortcut.length > 0 && <TooltipShortcut keys={payload.shortcut} />}\n                    </>\n                  )}\n                </BaseTooltip.Viewport>\n              </BaseTooltip.Popup>\n            </BaseTooltip.Positioner>\n          </BaseTooltip.Portal>\n        )}\n      </BaseTooltip.Root>\n    </GroupContext.Provider>\n  );\n}\n\nexport type TooltipGroupTriggerProps = Omit<BaseTooltip.Trigger.Props, \"handle\" | \"payload\" | \"render\" | \"children\"> & {\n  content: React.ReactNode;\n  shortcut?: string[];\n  /** The control, usually a toolbar button. It still needs its own aria-label when it shows only an icon. */\n  children: React.ReactElement<Record<string, unknown>>;\n};\n\nexport function TooltipGroupTrigger({ content, shortcut, children, ...rest }: TooltipGroupTriggerProps) {\n  const handle = useContext(GroupContext);\n  if (!handle) throw new Error(\"TooltipGroupTrigger must be inside a TooltipGroup\");\n  return (\n    <BaseTooltip.Trigger\n      handle={handle}\n      payload={{ content, shortcut }}\n      render={children}\n      aria-keyshortcuts={shortcut ? toAriaShortcut(shortcut) : undefined}\n      {...rest}\n    />\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "docs": "https://www.stealth.pm/ui/tooltip"
}
