{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"tooltip","type":"registry:component","title":"Tooltip","description":"Short supporting text for a control: opens after a moment on hover and at once on keyboard focus; inside a toolbar group the next one opens instantly.","categories":["overlays"],"dependencies":["@radix-ui/react-direction@1.1.4","@radix-ui/react-tooltip@1.2.16","clsx@^2.1.1","tailwind-merge@^3.7.0"],"registryDependencies":["https://beamline.io/registry/look-prism.json"],"files":[{"path":"beamline/components/tooltip/defaults.ts","type":"registry:file","target":"src/ui/beamline/components/tooltip/defaults.ts","content":"/** Pure allowed presentation defaults for Tooltip; no component or runtime imports. */\nexport interface TooltipDefaults {\n  /** Shared align setting; omit to inherit the surrounding scope or the component default. */\n  align?: \"start\" | \"center\" | \"end\";\n}\n"},{"path":"beamline/components/tooltip/tooltip.module.css","type":"registry:file","target":"src/ui/beamline/components/tooltip/tooltip.module.css","content":"@layer properties, theme, base, components, utilities;\n@layer components {\n  /* A tooltip arrives from its control: it starts a step toward the control on the side it opens on and moves away\n     into place as it fades in (--pui-motion-in), so on any side it reads as coming out of what it explains. Inside a\n     group, the next one opens at once where the last one stood and travels to its own control (--pui-duration-fast),\n     while the last one goes in the arrival time: one tooltip moving along the toolbar, not two blinking. It leaves\n     over the departure fade. Reduced motion keeps the fades and drops the travel (--pui-motion-travel is 0). */\n  .tip {\n    --_dx: 0px;\n    --_dy: 0px;\n    --_tx: 0px;\n    --_ty: 0px;\n    z-index: var(--pui-z-tooltip);\n    transform-origin: var(--radix-tooltip-content-transform-origin);\n  }\n  .tip[data-side=\"top\"] {\n    --_dy: var(--pui-space-1);\n  }\n  .tip[data-side=\"bottom\"] {\n    --_dy: calc(-1 * var(--pui-space-1));\n  }\n  .tip[data-side=\"left\"] {\n    --_dx: var(--pui-space-1);\n  }\n  .tip[data-side=\"right\"] {\n    --_dx: calc(-1 * var(--pui-space-1));\n  }\n  .tip[data-state=\"delayed-open\"] {\n    animation: tip-in var(--pui-motion-in) var(--pui-ease-default);\n  }\n  .tip[data-state=\"instant-open\"] {\n    animation: tip-fade var(--pui-fade-in) var(--pui-ease-default);\n  }\n  .tip[data-state=\"instant-open\"][data-travel] {\n    animation: tip-travel var(--pui-duration-fast) var(--pui-ease-standard);\n  }\n  .tip[data-state=\"closed\"] {\n    animation: tip-out var(--pui-fade-out) var(--pui-ease-default) forwards;\n  }\n  .tip[data-grouped][data-state=\"closed\"] {\n    animation-duration: var(--pui-fade-in);\n  }\n}\n@keyframes tip-in {\n  from {\n    opacity: 0;\n    translate: calc(var(--_dx) * var(--pui-motion-travel, 1)) calc(var(--_dy) * var(--pui-motion-travel, 1));\n  }\n}\n@keyframes tip-fade {\n  from {\n    opacity: 0;\n  }\n}\n@keyframes tip-travel {\n  from {\n    translate: calc(var(--_tx) * var(--pui-motion-travel, 1)) calc(var(--_ty) * var(--pui-motion-travel, 1));\n  }\n}\n@keyframes tip-out {\n  to {\n    opacity: 0;\n  }\n}\n"},{"path":"beamline/components/tooltip/tooltip.tsx","type":"registry:file","target":"src/ui/beamline/components/tooltip/tooltip.tsx","content":"\"use client\";\n\nimport { useComponentDefaults } from \"../../runtime/appearance\";\nimport type { TooltipDefaults } from \"./defaults\";\nexport type { TooltipDefaults } from \"./defaults\";\n\nimport {\n  createContext,\n  useContext,\n  useRef,\n  useState,\n  type CSSProperties,\n  type Ref,\n  type ReactElement,\n  type ReactNode,\n  type RefObject,\n} from \"react\";\nimport * as RadixTooltip from \"@radix-ui/react-tooltip\";\nimport { usePortalContainer } from \"../../runtime/appearance\";\nimport { cn } from \"../../lib/utils\";\nimport styles from \"./tooltip.module.css\";\n\n/** How long after one tooltip of a group closes the next opens at once (and travels from where the last one was). */\nconst SKIP = 400;\n/** The last control of a group whose tooltip showed: its centre and when its tooltip opened or closed. */\ntype Last = { el: Element; x: number; y: number; at: number };\nconst Grouped = createContext<RefObject<Last | null> | null>(null);\n\n/** Tooltips inside a group open instantly one after another (a toolbar): only the first waits, and each next one\n *  travels from where the last one stood. */\nexport function TooltipGroup({\n  children,\n  delay = 500,\n}: {\n  /** Content rendered inside the component. Required. */\n  children: ReactNode;\n  /** Milliseconds before the first tooltip in the group opens. Defaults to 500. */\n  delay?: number;\n}) {\n  const last = useRef<Last | null>(null);\n  return (\n    <RadixTooltip.Provider delayDuration={delay} skipDelayDuration={SKIP}>\n      <Grouped.Provider value={last}>{children}</Grouped.Provider>\n    </RadixTooltip.Provider>\n  );\n}\n\nexport interface TooltipClassNames {\n  /** Classes for the root part; no override by default. */\n  root?: string;\n  /** Classes for the trigger part; no override by default. */\n  trigger?: string;\n  /** Classes for the shortcut part; no override by default. */\n  shortcut?: string;\n}\n\nexport interface TooltipProps {\n  /** What the control does; a few words. */\n  content: ReactNode;\n  /** A keyboard shortcut shown after the text, e.g. \"⌘B\". */\n  shortcut?: string;\n  /** The control: one element that can take a ref (a button). */\n  children: ReactElement;\n  /** Edge from which the panel or anchored content opens. Defaults to \"top\". */\n  side?: \"top\" | \"right\" | \"bottom\" | \"left\";\n  /** Alignment within the available space. Defaults to \"center\". */\n  align?: \"start\" | \"center\" | \"end\";\n  /** Open state, controlled or uncontrolled; the tooltip opens on hover and on keyboard focus either way. */\n  open?: boolean;\n  /** Initial open state when uncontrolled. Defaults to false. */\n  defaultOpen?: boolean;\n  /** Reports open state after a change; no callback by default. */\n  onOpenChange?: (open: boolean) => void;\n  /** Classes on the documented visual root; no override by default. */\n  className?: string;\n  /** Classes for the named visual parts; no part overrides by default. */\n  classNames?: TooltipClassNames;\n  /** Inline styles on the documented visual root; no inline overrides by default. */\n  style?: CSSProperties;\n  /** Receives the documented root element; no ref by default. */\n  ref?: Ref<HTMLDivElement>;\n}\n\n/**\n * Short supporting text for a control: it opens on hover after a moment and at once on keyboard focus, and closes on\n * Escape. It arrives out of its control on whichever side it opens. Inside a TooltipGroup the next one opens instantly\n * and travels from where the last one stood. Never put essential information or anything clickable in a tooltip.\n */\nexport function Tooltip({\n  content,\n  shortcut,\n  children,\n  side = \"top\",\n  align: alignProp,\n  open,\n  defaultOpen,\n  onOpenChange,\n  className,\n  classNames,\n  style,\n  ref,\n}: TooltipProps) {\n  const scopeDefaults = useComponentDefaults<TooltipDefaults>(\"tooltip\", [\"align\"]);\n  const align = alignProp ?? scopeDefaults.align ?? \"center\";\n\n  const group = useContext(Grouped);\n  const container = usePortalContainer();\n  const trigger = useRef<HTMLButtonElement>(null); // the control, whatever element it is (Radix types it as a button)\n  // in a group, how far this tooltip is from the last one, so it can start there and travel to its own control\n  const [travel, setTravel] = useState<{ x: number; y: number } | null>(null);\n  const change = (next: boolean) => {\n    const el = trigger.current;\n    if (group && el) {\n      const r = el.getBoundingClientRect();\n      const x = r.left + r.width / 2;\n      const y = r.top + r.height / 2;\n      const last = group.current;\n      if (next) {\n        const near = last && last.el !== el && performance.now() - last.at < SKIP;\n        setTravel(near ? { x: last.x - x, y: last.y - y } : null);\n        group.current = { el, x, y, at: Infinity };\n      } else if (last?.el === el) last.at = performance.now();\n    }\n    onOpenChange?.(next);\n  };\n  const tip = (\n    <RadixTooltip.Root open={open} defaultOpen={defaultOpen} onOpenChange={change}>\n      <RadixTooltip.Trigger ref={trigger} asChild data-slot=\"tooltip-trigger\" className={classNames?.trigger}>\n        {children}\n      </RadixTooltip.Trigger>\n      {container !== null && (\n        <RadixTooltip.Portal container={container}>\n          <RadixTooltip.Content\n            data-slot=\"tooltip\"\n            data-grouped={group ? \"\" : undefined}\n            data-travel={travel ? \"\" : undefined}\n            style={travel ? ({ \"--_tx\": `${travel.x}px`, \"--_ty\": `${travel.y}px`, ...style } as CSSProperties) : style}\n            ref={ref}\n            side={side}\n            align={align}\n            sideOffset={6}\n            collisionPadding={8}\n            className={cn(\n              \"flex max-w-[min(260px,calc(100vw-16px))] items-center gap-2 rounded-chip bg-tooltip px-2 py-1 text-xs leading-snug text-tooltip-fg shadow-overlay\",\n              styles.tip,\n              classNames?.root,\n              className,\n            )}\n          >\n            {content}\n            {shortcut && (\n              <kbd\n                data-slot=\"tooltip-shortcut\"\n                className={cn(\"font-mono text-2xs text-[var(--pui-tooltip-muted)]\", classNames?.shortcut)}\n              >\n                {shortcut}\n              </kbd>\n            )}\n          </RadixTooltip.Content>\n        </RadixTooltip.Portal>\n      )}\n    </RadixTooltip.Root>\n  );\n  return group ? tip : <RadixTooltip.Provider delayDuration={500}>{tip}</RadixTooltip.Provider>;\n}\n\nexport default Tooltip;\n"},{"path":"beamline/lib/utils.ts","type":"registry:file","target":"src/ui/beamline/lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\";\nimport { extendTailwindMerge } from \"tailwind-merge\";\n\n/* The type roles (utilities.css) and the 2xs step are sizes, not text colours: without this, tailwind-merge reads\n   `text-overline` as a colour and drops it next to `text-ink-3`. A role and a size step still replace each other.\n   Likewise `font-title` is the look's title weight, not a family: read as a family it dropped `font-display` beside it,\n   and every merged page title lost the look's display face. */\nconst twMerge = extendTailwindMerge({\n  extend: {\n    classGroups: {\n      \"font-weight\": [{ font: [\"title\"] }],\n      \"font-family\": [{ font: [\"display\", \"numeric\", \"figure\"] }],\n      \"font-size\": [\n        {\n          text: [\n            \"2xs\",\n            \"display\",\n            \"title\",\n            \"heading\",\n            \"subheading\",\n            \"body\",\n            \"body-sm\",\n            \"reading\",\n            \"label\",\n            \"caption\",\n            \"overline\",\n            \"numeric\",\n            \"code\",\n          ],\n        },\n      ],\n    },\n  },\n});\n\n/** Merge class names, letting later Tailwind utilities win over earlier ones. */\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n\n/** A small seeded random generator (LCG): the same seed gives the same sequence, for stable sample data and jitter. */\nexport function rng(seed = 1) {\n  let s = seed >>> 0 || 1;\n  return () => (s = (s * 1664525 + 1013904223) >>> 0) / 2 ** 32;\n}\n\nexport const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));\n"}],"docs":"Tooltip is in src/ui/beamline/components/tooltip/tooltip.tsx. The source is in src/ui/beamline (ui/beamline in a project without src/). After Tailwind, import its styles/beamline.css once, then styles/look-prism.css in your app stylesheet (use relative paths). Keep the shared base before all chosen looks. Wrap your app in <UIRoot preset={prism}> with UIRoot from its runtime/appearance and prism from its looks/prism.  shadcn replaces files when asked; use the Beamline managed-source CLI from the start for three-way upgrades that preserve edits.","meta":{"free":true,"version":"0.2.0-rc.42","library":"https://beamline.io/components/tooltip"}}