{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tooltip",
  "type": "registry:ui",
  "title": "Tooltip",
  "description": "Fluent 2-styled tooltip with shadcn APIs. Built on @base-ui/react's Tooltip for hover/focus delay, portalled positioning and dismiss-on-Escape; the flyout is a neutral bg-popover surface with Fluent shadow-16 elevation rather than shadcn's inverted brand bubble, and ships with no arrow by default.",
  "author": "graundtech <https://github.com/graundtech/fluent2-react-kit>",
  "dependencies": [
    "@base-ui/react"
  ],
  "registryDependencies": [
    "https://fluent2-react-kit.graund.io/r/utils.json"
  ],
  "files": [
    {
      "path": "components/ui/tooltip.tsx",
      "type": "registry:ui",
      "target": "components/ui/tooltip.tsx",
      "content": "import { Tooltip as TooltipPrimitive } from \"@base-ui/react/tooltip\";\nimport type { ComponentProps } from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\n/**\n * Tooltip — Fluent 2-styled, shadcn-API tooltip.\n *\n * Behavior — hover/focus delay, portalling, collision-aware positioning,\n * dismiss-on-Escape — genuinely needs a primitive (conventions §9), so the\n * parts wrap `@base-ui/react/tooltip` (namespace import, matching the actual\n * export shape in node_modules, exactly like `select.tsx`/`avatar.tsx`).\n * `select.tsx`'s doc comment calls out Tooltip by name as inheriting its\n * portal → positioner → popup pattern; this file is that follow-through.\n *\n * ## Base UI mapping (conventions §9)\n *\n * | Exported (shadcn name) | Base UI primitive                                    |\n * | ----------------------- | ---------------------------------------------------- |\n * | `TooltipProvider`       | `Tooltip.Provider`                                    |\n * | `Tooltip`                | `Tooltip.Root`                                        |\n * | `TooltipTrigger`         | `Tooltip.Trigger`                                     |\n * | `TooltipContent`         | `Tooltip.Portal` + `Tooltip.Positioner` + `Tooltip.Popup` |\n *\n * ## Divergences from the shadcn/Radix Tooltip API (all deliberate)\n * 1. **Neutral elevated surface, not an inverted brand bubble.** shadcn's\n *    tooltip is `bg-primary text-primary-foreground` — a dark, high-contrast\n *    chip. Fluent 2 tooltips are a **neutral flyout**, visually the same family\n *    as `SelectContent`/menus: `bg-popover text-popover-foreground`, a hairline\n *    `border`, and `shadow-16` elevation, just smaller (`text-xs`,\n *    `px-2.5 py-1.5`, `max-w-60`). This is the headline visual departure from\n *    shadcn for this component — everything else follows shadcn's API shape.\n * 2. **No arrow/beak by default.** Base UI ships `Tooltip.Arrow`, but Fluent 2's\n *    tooltip is a plain floating rectangle with no pointer back to the trigger.\n *    `TooltipContent` does not render one and `TooltipArrow` is intentionally\n *    not re-exported; a consumer who wants Radix/shadcn's pointer can compose\n *    `@base-ui/react/tooltip`'s `Arrow` part directly alongside this component.\n * 3. **Composition uses Base UI's `render` prop, not `asChild`.**\n *    `Tooltip.Trigger` renders its own `<button>` (Base UI's model — there is no\n *    Slot-style boolean). `TooltipTrigger`'s props type (`TooltipPrimitive.\n *    Trigger.Props` — used directly rather than `ComponentProps<typeof\n *    TooltipPrimitive.Trigger>`, which loses the `ref` type through Base UI's\n *    generic `<Payload>` call signature) already includes Base UI's `render`\n *    prop, so no extra plumbing is needed to point the trigger at another\n *    element (e.g. the kit's `Button`):\n *    `<TooltipTrigger render={<Button variant=\"outline\">Hover me</Button>} />`.\n *    This is the direct Base UI analogue of shadcn's\n *    `<TooltipTrigger asChild><Button /></TooltipTrigger>`.\n * 4. **`TooltipProvider` defaults `delay` to 600ms**, matching\n *    `Tooltip.Trigger`'s own built-in default — set explicitly here so the hover\n *    delay is documented and overridable in one obvious place\n *    (`<TooltipProvider delay={0}>` for instant-open test/demo scenarios). The\n *    Provider's actual job is *grouping*: once one tooltip in the tree has\n *    opened, sibling tooltips opened within `timeout` (Base UI default 400ms)\n *    skip their own delay and open instantly — useful for a toolbar of\n *    icon buttons. A `<Tooltip>` used outside any `<TooltipProvider>` still\n *    works (Base UI's trigger-level 600ms default applies), the Provider is\n *    only required for the grouping behavior and for tests that want `delay={0}`.\n * 5. **`Tooltip.Viewport` is not exposed.** It exists to animate content\n *    swapping when multiple triggers share a single tooltip instance — a\n *    pattern this kit's simple per-trigger API doesn't use. Out of scope for\n *    this pass; compose it directly from `@base-ui/react/tooltip` if needed.\n * 6. **`TooltipContent` sets `role=\"tooltip\"`; `aria-describedby` is a documented\n *    recipe, not automatic wiring.** Confirmed against Base UI's own docs\n *    (base-ui.com/react/components/tooltip): unlike Radix, Base UI's Tooltip\n *    is explicitly \"visual-only\" — it does **not** set `role=\"tooltip\"` on the\n *    popup or `aria-describedby` on the trigger, and its own guidance is to\n *    give the trigger an `aria-label` instead (\"the tooltip's trigger must\n *    have an `aria-label` attribute that closely matches the tooltip's\n *    content\"). This wrapper closes the first, cheap half of that gap itself\n *    — `TooltipContent`'s `Popup` always carries `role=\"tooltip\"` (a bare\n *    `<div>` otherwise has no accessible role at all) — but does not\n *    synthesize `aria-describedby` linkage, which would require id-sharing\n *    plumbing (context + `useState`/`useId`) between `TooltipTrigger` and\n *    `TooltipContent` that Base UI does not expose and that would force\n *    `\"use client\"` on this otherwise-server-safe wrapper (§2). Consumers who\n *    need the description programmatically associated (beyond an `aria-label`\n *    on the trigger) can wire it manually, the same way `select.tsx`\n *    documents its `aria-labelledby` labeling recipe: give `TooltipContent`\n *    an `id` and pass that same value as `aria-describedby` on\n *    `TooltipTrigger`. `tooltip.test.tsx` has a test exercising this recipe.\n *\n * ## `\"use client\"` — intentionally omitted\n * Every Base UI Tooltip part module (`Root`, `Trigger`, `Portal`, `Positioner`,\n * `Popup`, `Provider`) carries its own `'use client'` directive at the source\n * level (verified in the compiled package output, same check as `avatar.tsx`).\n * This wrapper imports no icons (`@fluentui/react-icons`, which *would* force\n * the directive per conventions §9 — see `select.tsx`) and defines no\n * hooks/handlers of its own; it only forwards props into already-client parts.\n * So it stays a plain, server-renderable module: a Server Component tree can\n * render these client children directly without the parent re-declaring the\n * directive. Confirmed via `pnpm typecheck` + the test suite.\n *\n * ## data-slot note\n * `Tooltip` (Root) and `TooltipProvider` render no DOM element of their own\n * (Root groups state; Provider is a pair of context providers), so neither\n * carries a `data-slot`. Every part that renders an element does —\n * `tooltip-trigger`, `tooltip-positioner`, `tooltip-content` — which is what\n * tests/consumers hook onto.\n */\n\nfunction TooltipProvider({\n  delay = 600,\n  ...props\n}: ComponentProps<typeof TooltipPrimitive.Provider>) {\n  return <TooltipPrimitive.Provider delay={delay} {...props} />;\n}\n\nfunction Tooltip(props: TooltipPrimitive.Root.Props) {\n  return <TooltipPrimitive.Root {...props} />;\n}\n\nfunction TooltipTrigger({\n  className,\n  ...props\n}: TooltipPrimitive.Trigger.Props) {\n  return (\n    <TooltipPrimitive.Trigger\n      data-slot=\"tooltip-trigger\"\n      className={cn(className)}\n      {...props}\n    />\n  );\n}\n\n/**\n * Content — the floating surface: `Portal` → `Positioner` → `Popup`. Fluent\n * flyout look (divergence 1): `bg-popover` + `border` + `shadow-16`,\n * `rounded-md`, small type (`text-xs`), compact padding, capped width\n * (`max-w-60`) so long copy wraps instead of stretching edge to edge. A\n * subtle scale+fade open/close animation rides Base UI's\n * `data-starting-style`/`data-ending-style` hooks with token durations/easings\n * (same recipe as `SelectContent`). Carries `role=\"tooltip\"` (divergence 6)\n * since Base UI's raw `Popup` has no accessible role of its own.\n */\nfunction TooltipContent({\n  className,\n  children,\n  sideOffset = 4,\n  side = \"top\",\n  align = \"center\",\n  ...props\n}: ComponentProps<typeof TooltipPrimitive.Popup> &\n  Pick<\n    ComponentProps<typeof TooltipPrimitive.Positioner>,\n    \"side\" | \"align\" | \"sideOffset\"\n  >) {\n  return (\n    <TooltipPrimitive.Portal>\n      <TooltipPrimitive.Positioner\n        data-slot=\"tooltip-positioner\"\n        sideOffset={sideOffset}\n        side={side}\n        align={align}\n        className=\"z-50 outline-none\"\n      >\n        <TooltipPrimitive.Popup\n          data-slot=\"tooltip-content\"\n          role=\"tooltip\"\n          className={cn(\n            // Borderless by spec: Fluent's tooltip stroke is TransparentStroke\n            // (edge defined by elevation alone) and its elevation tier is\n            // Shadow 08, one step below flyouts (Figma validation pass 2,\n            // node 9014:2662). px-3 = Fluent spacingHorizontalM (12px).\n            \"relative max-w-60 origin-[var(--transform-origin)] text-balance rounded-md bg-popover px-3 py-1.5 text-xs text-popover-foreground shadow-8 outline-none\",\n            // motion — subtle scale + fade on open (enter) / close (exit)\n            \"transition-[opacity,scale] duration-fast ease-decelerate-mid\",\n            \"data-starting-style:scale-95 data-starting-style:opacity-0\",\n            \"data-ending-style:scale-95 data-ending-style:opacity-0 data-ending-style:ease-accelerate-mid\",\n            className\n          )}\n          {...props}\n        >\n          {children}\n        </TooltipPrimitive.Popup>\n      </TooltipPrimitive.Positioner>\n    </TooltipPrimitive.Portal>\n  );\n}\n\nexport { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };\n"
    }
  ],
  "docs": "Base UI's Tooltip is intentionally \"visual-only\": it does not set `role=\"tooltip\"` on the popup or wire `aria-describedby` from the trigger to it (confirmed against base-ui.com's own docs, which recommend an `aria-label` on the trigger instead). This component sets `role=\"tooltip\"` on `TooltipContent` for you, but does not synthesize `aria-describedby` — that would need id-sharing plumbing Base UI doesn't expose and would force a client boundary onto an otherwise server-safe wrapper. If you need that explicit association, give `TooltipContent` an `id` and pass the same value as `aria-describedby` on `TooltipTrigger` (mirrors the `aria-labelledby` recipe documented in the `select` item). For icon-only triggers, prefer an `aria-label` on the trigger — Base UI's own recommendation.\n\nCompose the trigger with another element (e.g. this kit's `Button`) via Base UI's `render` prop rather than an `asChild` boolean: `<TooltipTrigger render={<Button>Hover me</Button>} />`.\n\nWrap tooltips in a `TooltipProvider` (defaults `delay` to 600ms) so adjacent tooltips in the same tree open instantly once one is shown."
}
