{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"treemap","type":"registry:component","title":"Treemap","description":"Proportional tiles that morph with your data, with value and share tooltips.","author":"Saurabh <saurabh10102@gmail.com>","dependencies":["@floating-ui/dom","clsx","motion","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/charts/treemap.tsx","type":"registry:component","target":"@components/charts/treemap.tsx","content":"\"use client\";\n// beui.dev/charts/treemap\n\nimport { cn } from \"@/lib/utils\";\nimport { TreemapContext, useTreemapModel, type TreemapProps } from \"./treemap/context\";\nimport { TreemapLegend } from \"./treemap/legend\";\nimport { TreemapPlot } from \"./treemap/plot\";\nimport { TreemapTooltip } from \"./treemap/tooltip\";\n\n/** Compose Plot, Tooltip and Legend, or omit children for the complete chart. */\nexport function Treemap({\n  data,\n  label,\n  formatValue,\n  colors,\n  textColor,\n  activeId,\n  defaultActiveId,\n  onActiveChange,\n  className,\n  children,\n  ...props\n}: TreemapProps) {\n  const model = useTreemapModel({\n    data,\n    label,\n    formatValue,\n    colors,\n    textColor,\n    activeId,\n    defaultActiveId,\n    onActiveChange,\n  });\n  return (\n    <TreemapContext.Provider value={model}>\n      <section\n        aria-label={model.label}\n        {...props}\n        data-slot=\"treemap\"\n        className={cn(\"min-w-0 w-full space-y-4\", className)}\n      >\n        {children === undefined ? (\n          <>\n            <TreemapPlot />\n            <TreemapTooltip />\n            <TreemapLegend />\n          </>\n        ) : (\n          children\n        )}\n        <div className=\"sr-only\">\n          <table>\n            <caption>{model.label} data</caption>\n            <thead>\n              <tr>\n                <th scope=\"col\">Item</th>\n                <th scope=\"col\">Value</th>\n                <th scope=\"col\">Share of total</th>\n                <th scope=\"col\">Details</th>\n              </tr>\n            </thead>\n            <tbody>\n              {model.items.map((item) => (\n                <tr key={item.id}>\n                  <th scope=\"row\">{item.path.join(\" / \")}</th>\n                  <td>{model.formatValue(item.value)}</td>\n                  <td>{(item.share * 100).toFixed(1)}%</td>\n                  <td>{item.description ?? \"\"}</td>\n                </tr>\n              ))}\n            </tbody>\n          </table>\n        </div>\n      </section>\n    </TreemapContext.Provider>\n  );\n}\n\nexport { useTreemap } from \"./treemap/context\";\nexport type { TreemapProps } from \"./treemap/context\";\nexport type {\n  TreemapNode,\n  TreemapLeaf,\n  TreemapGroup,\n  TreemapItem,\n  TreemapTile,\n} from \"./treemap/model\";\nexport { TreemapPlot } from \"./treemap/plot\";\nexport type { TreemapPlotProps } from \"./treemap/plot\";\nexport { TreemapTooltip } from \"./treemap/tooltip\";\nexport type { TreemapTooltipProps } from \"./treemap/tooltip\";\nexport { TreemapLegend } from \"./treemap/legend\";\nexport type { TreemapLegendProps } from \"./treemap/legend\";\n"},{"path":"components/charts/treemap/context.ts","type":"registry:component","target":"@components/charts/treemap/context.ts","content":"\"use client\";\n\nimport { useReducedMotion } from \"motion/react\";\nimport {\n  createContext,\n  useCallback,\n  useContext,\n  useId,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n  type ComponentProps,\n} from \"react\";\nimport { useHoverCapable } from \"@/lib/hooks/use-hover-capable\";\nimport { useRowCursor } from \"@/lib/hooks/use-row-cursor\";\nimport { buildTreemap, layoutTreemap, TREEMAP_COLORS, type TreemapNode } from \"./model\";\n\nconst defaultFormat = (value: number) =>\n  value.toLocaleString(\"en-US\", { maximumFractionDigits: 2 });\n\nexport interface TreemapProps extends ComponentProps<\"section\"> {\n  /** Flat leaves or nested groups. Tile area uses leaf values; groups sum their descendants. */\n  data: readonly TreemapNode[];\n  label?: string;\n  formatValue?: (value: number) => string;\n  /** Top-level palette; nodes can override their inherited color and textColor. */\n  colors?: readonly string[];\n  textColor?: string;\n  activeId?: string | null;\n  defaultActiveId?: string | null;\n  onActiveChange?: (id: string | null) => void;\n}\n\nexport function useTreemapModel({\n  data,\n  label = \"Treemap\",\n  formatValue = defaultFormat,\n  colors = TREEMAP_COLORS,\n  textColor = \"#ffffff\",\n  activeId,\n  defaultActiveId = null,\n  onActiveChange,\n}: TreemapProps) {\n  const model = useMemo(() => buildTreemap(data, colors, textColor), [data, colors, textColor]);\n  const [size, setSize] = useState({ width: 1000, height: 625 });\n  const tiles = useMemo(\n    () => layoutTreemap(model.nodes, size.width, size.height),\n    [model.nodes, size],\n  );\n  const cursor = useRowCursor(tiles, \"\");\n  const [internalId, setInternalId] = useState(defaultActiveId);\n  if (internalId !== null && !tiles.some((tile) => tile.id === internalId)) setInternalId(null);\n  const requestedId = activeId === undefined ? internalId : activeId;\n  const active = tiles.find((tile) => tile.id === requestedId) ?? null;\n  const [tooltipOpen, setTooltipOpen] = useState(false);\n  const plotRef = useRef<HTMLDivElement>(null);\n  const buttons = useRef(new Map<string, HTMLButtonElement>());\n  const tooltipId = useId();\n  const latest = useRef({ activeId, requestedId, onActiveChange });\n  useLayoutEffect(() => {\n    latest.current = { activeId, requestedId, onActiveChange };\n  });\n  const setActive = useCallback((id: string | null) => {\n    const current = latest.current;\n    if (current.activeId === undefined) setInternalId(id);\n    if (current.requestedId !== id) current.onActiveChange?.(id);\n  }, []);\n  const registerButton = useCallback((id: string, node: HTMLButtonElement | null) => {\n    if (node) buttons.current.set(id, node);\n    else buttons.current.delete(id);\n  }, []);\n  return {\n    ...model,\n    tiles,\n    size,\n    setSize,\n    label,\n    formatValue,\n    active,\n    setActive,\n    cursor,\n    tooltipOpen,\n    setTooltipOpen,\n    plotRef,\n    buttons,\n    registerButton,\n    tooltipId,\n    reduce: useReducedMotion(),\n    canHover: useHoverCapable(),\n  };\n}\n\nexport const TreemapContext = createContext<ReturnType<typeof useTreemapModel> | null>(null);\n\nexport function useTreemap() {\n  const context = useContext(TreemapContext);\n  if (!context) throw new Error(\"Treemap parts must be rendered inside Treemap.\");\n  return context;\n}\n"},{"path":"components/charts/treemap/legend.tsx","type":"registry:component","target":"@components/charts/treemap/legend.tsx","content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useTreemap } from \"./context\";\n\nexport interface TreemapLegendProps {\n  className?: string;\n  showValues?: boolean;\n}\n\nexport function TreemapLegend({ className, showValues = false }: TreemapLegendProps) {\n  const { nodes, formatValue } = useTreemap();\n  if (!nodes.length) return null;\n  return (\n    <ul\n      aria-label=\"Treemap legend\"\n      className={cn(\"flex flex-wrap gap-x-5 gap-y-2 text-xs\", className)}\n    >\n      {nodes.map((node) => (\n        <li key={node.id} className=\"inline-flex items-center gap-1.5\">\n          <span\n            aria-hidden=\"true\"\n            className=\"size-1.5 shrink-0 rounded-full\"\n            style={{ backgroundColor: node.color }}\n          />\n          <span className=\"text-muted-foreground\">{node.label}</span>\n          {showValues ? (\n            <span className=\"font-mono tabular-nums\">{formatValue(node.value)}</span>\n          ) : null}\n        </li>\n      ))}\n    </ul>\n  );\n}\n"},{"path":"components/charts/treemap/model.ts","type":"registry:component","target":"@components/charts/treemap/model.ts","content":"interface TreemapBase {\n  /** Unique across the whole tree. Retain IDs when updating values. */\n  id: string;\n  label: string;\n  color?: string;\n  textColor?: string;\n  description?: string;\n}\n\nexport interface TreemapLeaf extends TreemapBase {\n  /** Finite, nonnegative weight. Zero values stay in the accessible table. */\n  value: number;\n}\n\nexport interface TreemapGroup extends TreemapBase {\n  /** Group weights are derived from their leaves; groups never add their own value. */\n  children: readonly TreemapNode[];\n}\n\nexport type TreemapNode = TreemapLeaf | TreemapGroup;\n\nexport interface TreemapItem extends TreemapLeaf {\n  color: string;\n  textColor: string;\n  path: readonly string[];\n  share: number;\n}\n\ninterface ResolvedNode {\n  id: string;\n  label: string;\n  value: number;\n  color: string;\n  children: ResolvedNode[];\n  item: TreemapItem | null;\n}\n\nexport interface TreemapTile extends TreemapItem {\n  x: number;\n  y: number;\n  width: number;\n  height: number;\n}\n\nexport const TREEMAP_COLORS = [\n  \"#1d4ed8\",\n  \"#7c3aed\",\n  \"#0f766e\",\n  \"#b45309\",\n  \"#be123c\",\n  \"#475569\",\n] as const;\n\nexport function buildTreemap(\n  data: readonly TreemapNode[],\n  colors: readonly string[],\n  textColor: string,\n) {\n  if (!colors.length) throw new Error(\"Treemap: colors must contain at least one color.\");\n  const ids = new Set<string>();\n  const items: TreemapItem[] = [];\n  const resolve = (\n    node: TreemapNode,\n    path: readonly string[],\n    inheritedColor: string,\n    inheritedText: string,\n  ): ResolvedNode => {\n    if (ids.has(node.id)) throw new Error(`Treemap: duplicate node ID \"${node.id}\".`);\n    ids.add(node.id);\n    const color = node.color ?? inheritedColor;\n    const foreground = node.textColor ?? inheritedText;\n    const nextPath = [...path, node.label];\n    if (\"children\" in node) {\n      const children = node.children.map((child) => resolve(child, nextPath, color, foreground));\n      const value = children.reduce((sum, child) => sum + child.value, 0);\n      if (!Number.isFinite(value)) throw new Error(\"Treemap: group totals must remain finite.\");\n      return { id: node.id, label: node.label, value, color, children, item: null };\n    }\n    if (!Number.isFinite(node.value) || node.value < 0)\n      throw new Error(\"Treemap: leaf values must be finite and nonnegative.\");\n    const item: TreemapItem = { ...node, color, textColor: foreground, path: nextPath, share: 0 };\n    items.push(item);\n    return { id: node.id, label: node.label, value: node.value, color, children: [], item };\n  };\n  const nodes = data.map((node, index) =>\n    resolve(node, [], colors[index % colors.length], textColor),\n  );\n  const total = nodes.reduce((sum, node) => sum + node.value, 0);\n  if (!Number.isFinite(total)) throw new Error(\"Treemap: the total must remain finite.\");\n  for (const item of items) item.share = total ? item.value / total : 0;\n  return { nodes, items, total };\n}\n\n/** Order-preserving balanced binary tiling, recursively applied to groups.\n * See https://d3js.org/d3-hierarchy/treemap#treemapBinary for the tiling method.\n * The calculation is local and never generates values or mutates consumer data. */\nexport function layoutTreemap(nodes: readonly ResolvedNode[], width: number, height: number) {\n  const tiles: TreemapTile[] = [];\n  const partition = (\n    siblings: readonly ResolvedNode[],\n    x: number,\n    y: number,\n    w: number,\n    h: number,\n  ) => {\n    const positive = siblings.filter((node) => node.value > 0);\n    if (!positive.length) return;\n    if (positive.length === 1) {\n      const node = positive[0];\n      if (node.item) tiles.push({ ...node.item, x, y, width: w, height: h });\n      else partition(node.children, x, y, w, h);\n      return;\n    }\n    const total = positive.reduce((sum, node) => sum + node.value, 0);\n    let cut = 1;\n    let subtotal = positive[0].value;\n    for (let i = 1; i < positive.length - 1; i++) {\n      const next = subtotal + positive[i].value;\n      if (Math.abs(next - total / 2) > Math.abs(subtotal - total / 2)) break;\n      cut = i + 1;\n      subtotal = next;\n    }\n    const fraction = subtotal / total;\n    if (w >= h) {\n      const split = w * fraction;\n      partition(positive.slice(0, cut), x, y, split, h);\n      partition(positive.slice(cut), x + split, y, w - split, h);\n    } else {\n      const split = h * fraction;\n      partition(positive.slice(0, cut), x, y, w, split);\n      partition(positive.slice(cut), x, y + split, w, h - split);\n    }\n  };\n  partition(nodes, 0, 0, width, height);\n  return tiles.filter((tile) => tile.width > 0 && tile.height > 0);\n}\n"},{"path":"components/charts/treemap/plot.tsx","type":"registry:component","target":"@components/charts/treemap/plot.tsx","content":"\"use client\";\n\nimport { AnimatePresence, LayoutGroup } from \"motion/react\";\nimport { useId, useLayoutEffect, useRef, type KeyboardEvent, type ReactNode } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { useTreemap } from \"./context\";\nimport type { TreemapTile } from \"./model\";\nimport { TreemapCell } from \"./tile\";\n\nexport interface TreemapPlotProps {\n  /** Sets the plot height; default 20rem. The layout follows its measured dimensions. */\n  className?: string;\n  tileClassName?: string;\n  /** Return presentation content only; each tile already owns its button and accessible name. */\n  renderTile?: (tile: TreemapTile) => ReactNode;\n}\n\nexport function TreemapPlot({ className, tileClassName, renderTile }: TreemapPlotProps) {\n  const { tiles, items, plotRef, setSize, cursor, buttons, setActive, setTooltipOpen } =\n    useTreemap();\n  const layoutId = useId();\n  const focusedId = useRef<string | null>(null);\n  useLayoutEffect(() => {\n    const plot = plotRef.current;\n    if (!plot) return;\n    const measure = () => {\n      const { width, height } = plot.getBoundingClientRect();\n      if (width > 0 && height > 0)\n        setSize((size) =>\n          size.width === width && size.height === height ? size : { width, height },\n        );\n    };\n    measure();\n    const observer = new ResizeObserver(measure);\n    observer.observe(plot);\n    return () => observer.disconnect();\n  }, [plotRef, setSize]);\n  useLayoutEffect(() => {\n    const id = focusedId.current;\n    if (!id || tiles.some((tile) => tile.id === id)) return;\n    focusedId.current = null;\n    const lost = buttons.current.get(id);\n    if (document.activeElement === lost || document.activeElement === document.body) {\n      const replacement = tiles[cursor.activeIndex];\n      if (replacement) buttons.current.get(replacement.id)?.focus();\n      else plotRef.current?.focus();\n    }\n  }, [tiles, cursor.activeIndex, buttons, plotRef]);\n  const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n    if (!(event.target instanceof HTMLButtonElement)) return;\n    const id = event.target.dataset.treemapTile;\n    const current = tiles.find((tile) => tile.id === id);\n    if (!current) return;\n    if (event.key === \"Escape\") {\n      setTooltipOpen(false);\n      return;\n    }\n    if (![\"ArrowLeft\", \"ArrowRight\", \"ArrowUp\", \"ArrowDown\", \"Home\", \"End\"].includes(event.key))\n      return;\n    event.preventDefault();\n    let next = current;\n    if (event.key === \"Home\") next = tiles[0];\n    else if (event.key === \"End\") next = tiles[tiles.length - 1];\n    else {\n      const horizontal = event.key === \"ArrowLeft\" || event.key === \"ArrowRight\";\n      const direction = event.key === \"ArrowLeft\" || event.key === \"ArrowUp\" ? -1 : 1;\n      const cx = current.x + current.width / 2;\n      const cy = current.y + current.height / 2;\n      let best = Number.POSITIVE_INFINITY;\n      for (const tile of tiles) {\n        const dx = tile.x + tile.width / 2 - cx;\n        const dy = tile.y + tile.height / 2 - cy;\n        const forward = (horizontal ? dx : dy) * direction;\n        const cross = Math.abs(horizontal ? dy : dx);\n        const score = forward + cross * 2;\n        if (forward > 0.01 && score < best) {\n          next = tile;\n          best = score;\n        }\n      }\n    }\n    setTooltipOpen(true);\n    if (next.id === current.id) setActive(current.id);\n    else buttons.current.get(next.id)?.focus();\n  };\n  return (\n    <LayoutGroup id={layoutId}>\n      {/* biome-ignore lint/a11y/useSemanticElements: This group is a chart, not a collection of form fields. */}\n      <div\n        ref={plotRef}\n        role=\"group\"\n        aria-label=\"Treemap tiles\"\n        tabIndex={-1}\n        data-slot=\"treemap-plot\"\n        className={cn(\n          \"relative h-80 w-full min-w-0 rounded-lg outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n          className,\n        )}\n        onKeyDown={onKeyDown}\n        onFocus={(event) => {\n          if (event.target instanceof HTMLButtonElement)\n            focusedId.current = event.target.dataset.treemapTile ?? null;\n        }}\n        onBlur={(event) => {\n          if (!event.currentTarget.contains(event.relatedTarget)) {\n            focusedId.current = null;\n            setTooltipOpen(false);\n          }\n        }}\n        onPointerLeave={() => setTooltipOpen(false)}\n      >\n        <AnimatePresence initial={false}>\n          {tiles.map((tile) => (\n            <TreemapCell\n              key={tile.id}\n              tile={tile}\n              className={tileClassName}\n              renderTile={renderTile}\n            />\n          ))}\n        </AnimatePresence>\n        {!tiles.length ? (\n          <p className=\"flex h-full items-center justify-center text-sm text-muted-foreground\">\n            {items.length ? \"No positive values\" : \"No treemap data\"}\n          </p>\n        ) : null}\n      </div>\n    </LayoutGroup>\n  );\n}\n"},{"path":"components/charts/treemap/tooltip.tsx","type":"registry:component","target":"@components/charts/treemap/tooltip.tsx","content":"\"use client\";\n\nimport { MotionConfig } from \"motion/react\";\nimport type { ReactNode } from \"react\";\nimport { NumberTicker } from \"@/components/motion/number-ticker\";\nimport { Tooltip } from \"@/components/motion/tooltip\";\nimport { cn } from \"@/lib/utils\";\nimport { useTreemap } from \"./context\";\nimport type { TreemapItem } from \"./model\";\n\nexport interface TreemapTooltipProps {\n  className?: string;\n  side?: \"top\" | \"right\" | \"bottom\" | \"left\";\n  children?: ReactNode | ((item: TreemapItem) => ReactNode);\n}\n\nexport function TreemapTooltip({ className, side = \"top\", children }: TreemapTooltipProps) {\n  const { active, plotRef, size, tiles, tooltipId, tooltipOpen, setTooltipOpen, formatValue } =\n    useTreemap();\n  if (!tiles.length) return null;\n  return (\n    <Tooltip\n      id={tooltipId}\n      anchorRef={plotRef}\n      followCursor\n      anchorPoint={{\n        x: active ? (active.x + active.width / 2) / size.width : 0.5,\n        y: active ? (active.y + active.height / 2) / size.height : 0.5,\n      }}\n      open={tooltipOpen && active !== null}\n      onOpenChange={setTooltipOpen}\n      side={side}\n      className={cn(\"max-w-64\", className)}\n      content={\n        active\n          ? typeof children === \"function\"\n            ? children(active)\n            : (children ?? (\n                <MotionConfig transition={{ type: \"tween\" }}>\n                  <span className=\"flex flex-col gap-1.5 text-xs\">\n                    {active.path.length > 1 ? (\n                      <span className=\"text-muted-foreground\">\n                        {active.path.slice(0, -1).join(\" / \")}\n                      </span>\n                    ) : null}\n                    <span className=\"font-medium\">{active.label}</span>\n                    <span className=\"inline-flex items-center gap-2 font-mono tabular-nums\">\n                      <NumberTicker\n                        value={active.value}\n                        format={() => formatValue(active.value)}\n                        startOnView={false}\n                        duration={0.2}\n                        stagger={0}\n                      />\n                      <span className=\"text-muted-foreground\">·</span>\n                      <NumberTicker\n                        value={active.share * 100}\n                        format={() => `${(active.share * 100).toFixed(1)}%`}\n                        startOnView={false}\n                        duration={0.2}\n                        stagger={0}\n                        className=\"text-muted-foreground\"\n                      />\n                    </span>\n                    {active.description ? (\n                      <span className=\"text-muted-foreground\">{active.description}</span>\n                    ) : null}\n                  </span>\n                </MotionConfig>\n              ))\n          : null\n      }\n    />\n  );\n}\n"},{"path":"lib/utils.ts","type":"registry:lib","target":"@lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"},{"path":"lib/hooks/use-hover-capable.ts","type":"registry:hook","target":"@lib/hooks/use-hover-capable.ts","content":"\"use client\";\n\nimport { useEffect, useState } from \"react\";\n\n/**\n * Returns true only on devices that have a true hover (mouse / trackpad).\n * Touch devices fire phantom `:hover` on tap that sticks until tap-elsewhere\n * — gate hover-only effects (scale lifts, magnetic pulls) behind this.\n */\nexport function useHoverCapable() {\n  const [canHover, setCanHover] = useState(false);\n\n  useEffect(() => {\n    if (typeof window === \"undefined\" || !window.matchMedia) return;\n    const mq = window.matchMedia(\"(hover: hover) and (pointer: fine)\");\n    const update = () => setCanHover(mq.matches);\n    update();\n    mq.addEventListener?.(\"change\", update);\n    return () => mq.removeEventListener?.(\"change\", update);\n  }, []);\n\n  return canHover;\n}\n"},{"path":"lib/hooks/use-row-cursor.ts","type":"registry:hook","target":"@lib/hooks/use-row-cursor.ts","content":"\"use client\";\n\nimport { useCallback, useLayoutEffect, useRef, useState } from \"react\";\n\n/**\n * Where the keyboard or the pointer last moved to: the row's id, stamped with\n * the query it was placed under.\n */\ntype RowCursor = { id: string; query: string };\n\n/** The cursor's row, or -1 once the query has moved on or the row has left. */\nfunction indexOfCursor(\n  rows: readonly { id: string }[],\n  query: string,\n  cursor: RowCursor | null,\n) {\n  if (cursor === null || cursor.query !== query) return -1;\n  return rows.findIndex((row) => row.id === cursor.id);\n}\n\n/**\n * The highlighted row of a list whose rows can change under it.\n *\n * Which row is highlighted is resolved during render, never in a passive\n * effect: a passive effect runs after the commit, so a list that had just\n * changed would carry an `aria-activedescendant` naming a row that has left it,\n * and a key pressed in that window would commit the wrong row or nothing at\n * all.\n *\n * The cursor holds the row's id, not its position. A position alone cannot tell\n * a list that shrank from one that swapped its rows for a different set of the\n * same length, and the second case is the one that silently hands Enter to a\n * row the user never chose. A cursor whose row has left the list returns the\n * highlight to the first row rather than to the nearest surviving one: the row\n * the user aimed at is gone, and the first row is where a new query already\n * puts the highlight.\n *\n * Pass the query the list is filtered by. The cursor is stamped with it and\n * dropped when it changes, so a caller cannot forget to clear it — including a\n * caller whose query arrives as a prop and so never runs its own handler. Rows\n * that come back under a query that has moved on cannot revive it either.\n * `moveTo(null)` is for deliberate resets, such as reopening the list.\n *\n * Both callbacks keep one identity for the life of the component, and read the\n * rows and the query through a ref to do it. A caller will put them in an\n * effect's dependencies — the exhaustive-deps rule makes it — and a `moveTo`\n * rebuilt on every keystroke would re-run that effect on every keystroke.\n */\nexport function useRowCursor(rows: readonly { id: string }[], query: string) {\n  const [cursor, setCursor] = useState<RowCursor | null>(null);\n  // Written after commit, not during render: a render React discards or has not\n  // finished still runs the component body, and an event handler that read this\n  // in that window would stamp the cursor with a query the committed tree does\n  // not have.\n  const latest = useRef({ rows, query });\n  useLayoutEffect(() => {\n    latest.current = { rows, query };\n  });\n\n  const cursorRow = indexOfCursor(rows, query, cursor);\n  // Cleared rather than ignored: React re-runs this render with the cursor\n  // already gone, so rows that come back cannot revive a highlight the user has\n  // stopped aiming at.\n  if (cursor !== null && cursorRow < 0) setCursor(null);\n\n  const moveTo = useCallback(\n    (id: string | null) =>\n      setCursor(id === null ? null : { id, query: latest.current.query }),\n    [],\n  );\n\n  const moveActive = useCallback((direction: 1 | -1) => {\n    const { rows: live, query: liveQuery } = latest.current;\n    const last = live.length - 1;\n    if (last < 0) return;\n    // Steps from the row the cursor is really on, inside the update, so that\n    // two keys landing in one batch move two rows rather than one.\n    setCursor((current) => {\n      const at = Math.max(indexOfCursor(live, liveQuery, current), 0);\n      const next = Math.min(Math.max(at + direction, 0), last);\n      return { id: live[next].id, query: liveQuery };\n    });\n  }, []);\n\n  return { activeIndex: cursorRow < 0 ? 0 : cursorRow, moveTo, moveActive };\n}\n"},{"path":"components/charts/treemap/tile.tsx","type":"registry:component","target":"@components/charts/treemap/tile.tsx","content":"\"use client\";\n\nimport { motion, useIsPresent } from \"motion/react\";\nimport { useCallback, type ReactNode } from \"react\";\nimport { EASE_OUT, SPRING_LAYOUT } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\nimport { useTreemap } from \"./context\";\nimport type { TreemapTile } from \"./model\";\n\nexport function TreemapCell({\n  tile,\n  className,\n  renderTile,\n}: {\n  tile: TreemapTile;\n  className?: string;\n  renderTile?: (tile: TreemapTile) => ReactNode;\n}) {\n  const {\n    size,\n    active,\n    setActive,\n    cursor,\n    tiles,\n    formatValue,\n    tooltipId,\n    tooltipOpen,\n    setTooltipOpen,\n    reduce,\n    canHover,\n    registerButton,\n  } = useTreemap();\n  const present = useIsPresent();\n  const ref = useCallback(\n    (node: HTMLButtonElement | null) => registerButton(tile.id, node),\n    [tile.id, registerButton],\n  );\n  const selected = tooltipOpen && active?.id === tile.id;\n  const width = tile.width / size.width;\n  const height = tile.height / size.height;\n  // Small tiles remain inspectable; their labels stay in the accessible name\n  // and exact-data table rather than spilling into neighbouring rectangles.\n  const showLabel = tile.width >= 52 && tile.height >= 32;\n  const showValue = tile.width >= 70 && tile.height >= 56;\n  return (\n    <motion.button\n      ref={ref}\n      type=\"button\"\n      data-treemap-tile={tile.id}\n      layout={!reduce}\n      inert={!present}\n      aria-hidden={!present || undefined}\n      tabIndex={present && tiles[cursor.activeIndex]?.id === tile.id ? 0 : -1}\n      aria-label={[\n        tile.path.join(\" / \"),\n        formatValue(tile.value),\n        `${(tile.share * 100).toFixed(1)}% of total`,\n        tile.description,\n      ]\n        .filter(Boolean)\n        .join(\". \")}\n      aria-describedby={present && selected ? tooltipId : undefined}\n      className={cn(\n        \"absolute overflow-hidden border-2 border-background text-left outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-white\",\n        className,\n      )}\n      style={{\n        left: `${(tile.x / size.width) * 100}%`,\n        top: `${(tile.y / size.height) * 100}%`,\n        width: `${width * 100}%`,\n        height: `${height * 100}%`,\n        backgroundColor: tile.color,\n        color: tile.textColor,\n        borderRadius: 8,\n        pointerEvents: present ? \"auto\" : \"none\",\n      }}\n      initial={{ opacity: 0 }}\n      animate={{ opacity: 1 }}\n      exit={{ opacity: 0 }}\n      transition={{\n        layout: reduce ? { duration: 0 } : SPRING_LAYOUT,\n        opacity: { type: \"tween\", duration: present ? 0.18 : 0.1, ease: EASE_OUT },\n      }}\n      onPointerEnter={(event) => {\n        if (present && canHover && event.pointerType !== \"touch\") {\n          setActive(tile.id);\n          setTooltipOpen(true);\n        }\n      }}\n      onPointerMove={(event) => {\n        if (present && (event.pointerType === \"touch\" ? event.buttons === 1 : canHover)) {\n          setActive(tile.id);\n          setTooltipOpen(true);\n        }\n      }}\n      onPointerDown={() => {\n        if (present) {\n          setActive(tile.id);\n          setTooltipOpen(true);\n        }\n      }}\n      onClick={() => {\n        if (present) {\n          setActive(tile.id);\n          setTooltipOpen(true);\n        }\n      }}\n      onFocus={() => {\n        if (present) {\n          cursor.moveTo(tile.id);\n          setActive(tile.id);\n          setTooltipOpen(true);\n        }\n      }}\n    >\n      <motion.span\n        aria-hidden=\"true\"\n        layout={reduce ? false : \"position\"}\n        transition={reduce ? { duration: 0 } : SPRING_LAYOUT}\n        className=\"absolute inset-x-2 top-2 min-w-0 sm:inset-x-3 sm:top-3\"\n      >\n        {renderTile ? (\n          renderTile(tile)\n        ) : showLabel ? (\n          <>\n            <span className=\"block truncate text-xs font-semibold sm:text-sm\">{tile.label}</span>\n            {showValue ? (\n              <span className=\"mt-1 block truncate font-mono text-[10px] tabular-nums opacity-80 sm:text-xs\">\n                {formatValue(tile.value)}\n              </span>\n            ) : null}\n          </>\n        ) : null}\n      </motion.span>\n      <motion.span\n        aria-hidden=\"true\"\n        className=\"pointer-events-none absolute inset-0 bg-white/10\"\n        initial={false}\n        animate={{ opacity: selected ? 1 : 0 }}\n        transition={{ type: \"tween\", duration: 0.1, ease: EASE_OUT }}\n      />\n    </motion.button>\n  );\n}\n"},{"path":"components/motion/number-ticker.tsx","type":"registry:component","target":"@components/motion/number-ticker.tsx","content":"\"use client\";\n\nimport { animate, motion, useInView, useReducedMotion } from \"motion/react\";\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport { EASE_OUT } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface NumberTickerProps {\n  value: number;\n  /** Digits to pad to (left). */\n  pad?: number;\n  /** Per-digit roll duration in seconds. */\n  duration?: number;\n  /** Stagger between digits. */\n  stagger?: number;\n  /** Render only after the element enters the viewport. */\n  startOnView?: boolean;\n  prefix?: string;\n  suffix?: string;\n  /** Add a small blur during digit rolls. */\n  blur?: boolean;\n  className?: string;\n  digitClassName?: string;\n  /** Insert locale group separators (commas). Server-component safe. */\n  locale?: boolean;\n  /** Custom formatter. Client-only — server components must use `locale` instead. */\n  format?: (value: number) => string;\n}\n\nconst DIGIT_HEIGHT_EM = 1.1;\nconst DIGITS = Array.from({ length: 10 }, (_, n) => n);\n\nexport function NumberTicker({\n  value,\n  pad,\n  duration = 0.9,\n  stagger = 0.04,\n  startOnView = true,\n  prefix,\n  suffix,\n  blur = false,\n  className,\n  digitClassName,\n  locale,\n  format,\n}: NumberTickerProps) {\n  const containerRef = useRef<HTMLSpanElement>(null);\n  const inView = useInView(containerRef, { once: true, amount: 0.6 });\n  const [armed, setArmed] = useState(!startOnView);\n\n  useEffect(() => {\n    if (startOnView && inView) setArmed(true);\n  }, [startOnView, inView]);\n\n  const text = useMemo(() => {\n    const rounded = Math.round(value);\n    const formatted = format\n      ? format(rounded)\n      : locale\n        ? rounded.toLocaleString()\n        : rounded.toString();\n    return pad ? formatted.padStart(pad, \"0\") : formatted;\n  }, [value, pad, format, locale]);\n  const glyphs = useMemo(() => {\n    const chars = text.split(\"\");\n    // Key by place value (position from the right): a changing digit keeps its\n    // identity and rolls to the new value instead of remounting and replaying\n    // from 0. Growing numbers add glyphs on the left without re-keying the\n    // ones, tens, hundreds already on screen.\n    return chars.map((char, i) => ({ char, id: `g-${chars.length - 1 - i}` }));\n  }, [text]);\n  const readableText = `${prefix ?? \"\"}${text}${suffix ?? \"\"}`;\n\n  // Stagger is an entrance flourish. Once the reveal has played, value\n  // changes roll every digit immediately — a per-digit delay on live updates\n  // reads as lag.\n  const [entered, setEntered] = useState(false);\n  useEffect(() => {\n    if (!armed || entered) return;\n    const total = (duration + glyphs.length * stagger) * 1000;\n    const t = window.setTimeout(() => setEntered(true), total);\n    return () => window.clearTimeout(t);\n  }, [armed, entered, duration, stagger, glyphs.length]);\n\n  return (\n    <span\n      ref={containerRef}\n      className={cn(\"inline-flex items-center tabular-nums\", className)}\n    >\n      <span className=\"sr-only\">{readableText}</span>\n      <span aria-hidden=\"true\" className=\"inline-flex items-center\">\n        {prefix ? <span>{prefix}</span> : null}\n        {glyphs.map(({ char, id }, i) => {\n          const isDigit = /\\d/.test(char);\n          if (!isDigit) {\n            return (\n              <span key={id} className=\"inline-block\">\n                {char}\n              </span>\n            );\n          }\n          const digit = Number(char);\n          return (\n            <Digit\n              key={id}\n              digit={armed ? digit : 0}\n              delay={entered ? 0 : i * stagger}\n              duration={duration}\n              blur={blur}\n              className={digitClassName}\n            />\n          );\n        })}\n        {suffix ? <span>{suffix}</span> : null}\n      </span>\n    </span>\n  );\n}\n\nfunction Digit({\n  digit,\n  delay,\n  duration,\n  blur,\n  className,\n}: {\n  digit: number;\n  delay: number;\n  duration: number;\n  blur: boolean;\n  className?: string;\n}) {\n  const reduce = useReducedMotion();\n  const columnRef = useRef<HTMLSpanElement>(null);\n\n  useEffect(() => {\n    if (reduce || !blur || !columnRef.current || !Number.isFinite(digit)) {\n      return;\n    }\n\n    const node = columnRef.current;\n    const controls = animate(\n      node,\n      { filter: [\"blur(10px)\", \"blur(0px)\"] },\n      {\n        duration: Math.min(duration * 0.75, 0.32),\n        delay,\n        ease: EASE_OUT,\n      },\n    );\n\n    return () => {\n      controls.stop();\n      node.style.filter = \"blur(0px)\";\n    };\n  }, [blur, delay, digit, duration, reduce]);\n\n  return (\n    <span\n      className={cn(\"relative inline-block overflow-hidden\", className)}\n      style={{ height: `${DIGIT_HEIGHT_EM}em`, width: \"1ch\" }}\n    >\n      <motion.span\n        ref={columnRef}\n        initial={{ y: 0 }}\n        animate={{ y: `-${digit * DIGIT_HEIGHT_EM}em` }}\n        transition={\n          reduce\n            ? { duration: 0 }\n            : { duration, delay, ease: EASE_OUT }\n        }\n        className=\"absolute inset-x-0 top-0 flex flex-col items-center will-change-[transform,filter]\"\n      >\n        {DIGITS.map((n) => (\n          <span\n            key={n}\n            className=\"flex h-[1.1em] items-center justify-center leading-none\"\n          >\n            {n}\n          </span>\n        ))}\n      </motion.span>\n    </span>\n  );\n}\n"},{"path":"components/motion/tooltip.tsx","type":"registry:component","target":"@components/motion/tooltip.tsx","content":"\"use client\";\n\nimport { AnimatePresence } from \"motion/react\";\nimport { TooltipPositioner } from \"./tooltip/positioner\";\nimport { useTooltipPointer } from \"./tooltip/use-position\";\nimport {\n  cloneElement,\n  isValidElement,\n  type PointerEvent,\n  type ReactElement,\n  type ReactNode,\n  type RefObject,\n  useCallback,\n  useEffect,\n  useId,\n  useRef,\n  useState,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { TooltipSurface } from \"@/components/motion/tooltip-surface\";\nimport { useDismiss } from \"@/lib/hooks/use-dismiss\";\nimport { useHoverGesture } from \"@/lib/hooks/use-hover-gesture\";\nimport { useTapGesture } from \"@/lib/hooks/use-tap-gesture\";\nimport { cn } from \"@/lib/utils\";\n\ntype Side = \"top\" | \"right\" | \"bottom\" | \"left\";\n\nexport interface TooltipProps {\n  content: ReactNode;\n  children?: ReactElement;\n  /** Existing trigger for controlled integrations such as chart cells. */\n  anchorRef?: RefObject<HTMLElement | SVGElement | null>;\n  /** Point within the anchor, as fractions of its rendered width and height. */\n  anchorPoint?: { x: number; y: number };\n  /** Follow real pointer coordinates; keyboard focus still uses the anchor. */\n  followCursor?: boolean;\n  open?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  id?: string;\n  side?: Side;\n  /** Delay before showing (ms). Default 120. */\n  delay?: number;\n  className?: string;\n  /** Classes for the outer wrapper span. Use to fix baseline / fill parent. */\n  wrapperClassName?: string;\n}\n\n// Once any tooltip has just closed, neighbouring tooltips open without the\n// initial delay — moving along a toolbar feels instant after the first one.\nconst WARM_WINDOW_MS = 300;\nlet lastHiddenAt = 0;\n\nexport function Tooltip({\n  content,\n  children,\n  side = \"top\",\n  delay = 120,\n  className,\n  wrapperClassName,\n  anchorRef: externalAnchorRef,\n  anchorPoint,\n  followCursor = false,\n  open: controlledOpen,\n  onOpenChange,\n  id: providedId,\n}: TooltipProps) {\n  const [internalOpen, setInternalOpen] = useState(false);\n  const open = controlledOpen ?? internalOpen;\n  const setOpen = useCallback(\n    (next: boolean) => {\n      if (controlledOpen === undefined) setInternalOpen(next);\n      onOpenChange?.(next);\n    },\n    [controlledOpen, onOpenChange],\n  );\n  const generatedId = useId();\n  const id = providedId ?? generatedId;\n  const timer = useRef<ReturnType<typeof setTimeout> | null>(null);\n  const wrapperRef = useRef<HTMLSpanElement>(null);\n  const anchorRef = externalAnchorRef ?? wrapperRef;\n  const hover = useHoverGesture();\n  const floatingRef = useRef<HTMLSpanElement | null>(null);\n  const pointer = useTooltipPointer(anchorRef, followCursor);\n  const focused = useRef(false);\n\n  const show = useCallback(() => {\n    if (timer.current) clearTimeout(timer.current);\n    if (open) return;\n    const warm = Date.now() - lastHiddenAt < WARM_WINDOW_MS;\n    if (warm) {\n      setOpen(true);\n      return;\n    }\n    timer.current = setTimeout(() => {\n      setOpen(true);\n    }, delay);\n  }, [delay, setOpen, open]);\n\n  const hide = useCallback(() => {\n    if (timer.current) {\n      clearTimeout(timer.current);\n      timer.current = null;\n    }\n    if (open) lastHiddenAt = Date.now();\n    setOpen(false);\n  }, [open, setOpen]);\n\n  const leave = useCallback(() => {\n    if (focused.current) return;\n    if (timer.current) clearTimeout(timer.current);\n    // Bridge the small physical gap to a stationary, readable tooltip.\n    if (followCursor) hide();\n    else timer.current = setTimeout(hide, 100);\n  }, [followCursor, hide]);\n  const insideTooltip = useCallback(\n    (target: Element) => Boolean(floatingRef.current?.contains(target)),\n    [],\n  );\n\n  // A finger never hovers, and Safari does not focus a button on tap either, so\n  // the label is only reachable if the tap itself opens the tooltip. A click\n  // carries no pointerType, so the pointerdown that preceded it is what says\n  // whether this was a tap; keyboard activation arrives with no pointerdown at\n  // all, and focus has already shown the label there.\n  const tap = useTapGesture<boolean>();\n\n  const toggleOnTap = useCallback(() => {\n    const gesture = tap.take();\n    if (!gesture || gesture.pointerType === \"mouse\") return;\n    if (gesture.state) {\n      hide();\n      return;\n    }\n    if (timer.current) clearTimeout(timer.current);\n    setOpen(true);\n  }, [hide, tap, setOpen]);\n\n  // ...and closed again by the next tap that lands somewhere else. The label\n  // covers nothing interactive, so that tap passes through to what it hit.\n  useDismiss(open, hide, anchorRef, { ignore: insideTooltip });\n\n  useEffect(\n    () => () => {\n      if (timer.current) clearTimeout(timer.current);\n    },\n    [],\n  );\n\n  if (!externalAnchorRef && !isValidElement(children)) return children;\n\n  // The label describes the trigger, so it has to name the trigger itself.\n  // Everything else the tooltip needs is read off the anchor below instead of\n  // cloned on: a handler written onto the child is the child's handler as far\n  // as that child can tell, and a component that owns its activation —\n  // hard-wiring onClick and spreading the rest of its props over it, as\n  // ThemeToggle does — then runs the tooltip's instead of its own. Composing\n  // with `props.onClick` cannot save it either, because a component element's\n  // props hold nothing the component does internally.\n  const trigger = isValidElement(children)\n    ? cloneElement(children as ReactElement<Record<string, unknown>>, {\n        \"aria-describedby\":\n          [(children.props as Record<string, unknown>)[\"aria-describedby\"], open ? id : undefined]\n            .filter(Boolean)\n            .join(\" \") || undefined,\n      })\n    : null;\n\n  return (\n    <>\n      {!externalAnchorRef ? (\n        // biome-ignore lint/a11y/noStaticElementInteractions: This wrapper observes bubbling trigger events without replacing the control's handlers.\n        <span\n          ref={wrapperRef}\n          className={cn(\"relative inline-flex align-middle\", wrapperClassName)}\n          // Pointer events, not the mouse pair: a tap fires compatibility\n          // mouseenter/mouseleave that carry no pointerType, which raced the tap\n          // path into opening and closing the same label.\n          onPointerEnter={(event: PointerEvent) => {\n            if (hover.enter(event)) show();\n          }}\n          onPointerLeave={(event: PointerEvent) => {\n            if (hover.leave(event)) leave();\n          }}\n          onFocus={() => {\n            focused.current = true;\n            show();\n          }}\n          onBlur={() => {\n            focused.current = false;\n            hide();\n          }}\n          onPointerDown={(event: PointerEvent) => tap.start(event, open)}\n          // A gesture the platform took away sends no click, and a key press\n          // starts an activation that never had a pointer behind it. Either way\n          // the record has to go, or the next click reads a finger that has long\n          // since lifted.\n          onPointerCancel={tap.drop}\n          onKeyDown={(event) => {\n            tap.drop();\n            if (event.key === \"Escape\") hide();\n          }}\n          onClick={toggleOnTap}\n        >\n          {trigger}\n        </span>\n      ) : null}\n      {typeof document !== \"undefined\"\n        ? createPortal(\n            <AnimatePresence>\n              {open ? (\n                <TooltipPositioner\n                  key=\"tooltip\"\n                  anchorRef={anchorRef}\n                  floatingRef={floatingRef}\n                  anchorPoint={anchorPoint}\n                  followCursor={followCursor}\n                  side={side}\n                  onDismiss={hide}\n                  pointer={pointer}\n                >\n                  {(positioned, isPresent) => (\n                    <TooltipSurface\n                      id={id}\n                      ready={positioned}\n                      side={side}\n                      onPointerEnter={() => {\n                        if (timer.current) clearTimeout(timer.current);\n                      }}\n                      onPointerLeave={leave}\n                      style={{\n                        maxWidth: \"calc(100vw - 16px)\",\n                        whiteSpace: \"normal\",\n                        pointerEvents: isPresent && !followCursor ? \"auto\" : \"none\",\n                      }}\n                      className={cn(\"overflow-hidden\", className)}\n                    >\n                      {content}\n                    </TooltipSurface>\n                  )}\n                </TooltipPositioner>\n              ) : null}\n            </AnimatePresence>,\n            document.body,\n          )\n        : null}\n    </>\n  );\n}\n"},{"path":"lib/ease.ts","type":"registry:lib","target":"@lib/ease.ts","content":"// Shared motion tokens. Easing curves mirror the CSS custom properties in\n// globals.css; springs are the canonical physics used across components.\n// Strong custom variants — defaults like `ease-in`/`ease-out` feel weak.\n\nexport const EASE_OUT = [0.16, 1, 0.3, 1] as const;\nexport const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;\nexport const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;\n\n/** CSS string form of EASE_OUT for inline style transitions. */\nexport const EASE_OUT_CSS = \"cubic-bezier(0.16, 1, 0.3, 1)\";\n\n/** Press feedback on buttons and other tappable surfaces. */\nexport const SPRING_PRESS = {\n  type: \"spring\",\n  stiffness: 500,\n  damping: 30,\n  mass: 0.6,\n} as const;\n\n/** Content swaps — label/icon slots trading places inside a control. */\nexport const SPRING_SWAP = {\n  type: \"spring\",\n  stiffness: 460,\n  damping: 30,\n  mass: 0.55,\n} as const;\n\n/** Overlay panel entrances — modals and sheets summoned by pointer. */\nexport const SPRING_PANEL = {\n  type: \"spring\",\n  stiffness: 420,\n  damping: 40,\n  mass: 0.5,\n} as const;\n\n/** Shared-layout glides — pills, indicators and panels morphing between positions. */\nexport const SPRING_LAYOUT = {\n  type: \"spring\",\n  stiffness: 360,\n  damping: 32,\n  mass: 0.6,\n} as const;\n\n/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt, dock). */\nexport const SPRING_MOUSE = {\n  stiffness: 200,\n  damping: 15,\n  mass: 0.3,\n} as const;\n\n/** Dragged handles and fills (sliders) — critically damped `useSpring` config,\n * so the value follows the pointer butterily and never rebounds off an end. */\nexport const SPRING_GLIDE = {\n  stiffness: 700,\n  damping: 50,\n  mass: 0.5,\n} as const;\n"},{"path":"components/motion/tooltip/positioner.tsx","type":"registry:component","target":"@components/motion/tooltip/positioner.tsx","content":"\"use client\";\n\nimport { useIsPresent } from \"motion/react\";\nimport { useCallback, useState, type ReactNode } from \"react\";\nimport { useTooltipPosition } from \"./use-position\";\n\ntype PositionProps = Parameters<typeof useTooltipPosition>[0];\n\n/** Readiness lives with the mounted overlay, not a trigger ref's attach/detach cycle. */\nexport function TooltipPositioner({\n  children,\n  ...position\n}: Omit<PositionProps, \"open\" | \"onPosition\"> & {\n  children: (ready: boolean, present: boolean) => ReactNode;\n}) {\n  const present = useIsPresent();\n  const [ready, setReady] = useState(false);\n  const onPosition = useCallback(() => setReady(true), []);\n  useTooltipPosition({ ...position, open: present, onPosition });\n  return (\n    <span\n      ref={position.floatingRef}\n      inert={!present}\n      aria-hidden={!present || undefined}\n      className=\"pointer-events-none fixed left-0 top-0 z-[9999] w-max\"\n      style={{ visibility: \"hidden\", maxWidth: \"calc(100vw - 16px)\" }}\n    >\n      {children(ready, present)}\n    </span>\n  );\n}\n"},{"path":"components/motion/tooltip/use-position.ts","type":"registry:component","target":"@components/motion/tooltip/use-position.ts","content":"\"use client\";\n\nimport {\n  autoUpdate,\n  computePosition,\n  flip,\n  offset,\n  shift,\n  type Placement,\n  type VirtualElement,\n} from \"@floating-ui/dom\";\nimport { useCallback, useLayoutEffect, useRef, type RefObject } from \"react\";\n\nexport type TooltipSide = \"top\" | \"right\" | \"bottom\" | \"left\";\nexport type TooltipPoint = { x: number; y: number };\n\n/** Position is geometry, not animation. One write per frame, never a spring chasing a pointer. */\nexport function useTooltipPosition({\n  open,\n  anchorRef,\n  floatingRef,\n  anchorPoint,\n  followCursor,\n  side,\n  onDismiss,\n  onPosition,\n  pointer,\n}: {\n  open: boolean;\n  anchorRef: RefObject<HTMLElement | SVGElement | null>;\n  floatingRef: RefObject<HTMLSpanElement | null>;\n  anchorPoint?: TooltipPoint;\n  followCursor: boolean;\n  side: TooltipSide;\n  onDismiss: () => void;\n  onPosition: () => void;\n  pointer: ReturnType<typeof useTooltipPointer>;\n}) {\n  const { cursor, onMove } = pointer;\n  const cursorSide = useRef<Placement | null>(null);\n  useLayoutEffect(() => {\n    cursorSide.current = open ? side : null;\n  }, [open, side]);\n  const frame = useRef<number | null>(null);\n  const version = useRef(0);\n  const update = useRef<() => void>(() => {});\n  const schedule = useCallback(() => {\n    // Invalidate older async calculations as soon as new geometry is requested.\n    version.current++;\n    if (frame.current !== null) return;\n    frame.current = requestAnimationFrame(() => {\n      frame.current = null;\n      update.current();\n    });\n  }, []);\n  useLayoutEffect(() => {\n    onMove.current = schedule;\n    return () => {\n      onMove.current = null;\n    };\n  }, [onMove, schedule]);\n  const pointX = anchorPoint?.x;\n  const pointY = anchorPoint?.y;\n\n  // Latest committed inputs are read without recreating observers for every period/content update.\n  useLayoutEffect(() => {\n    update.current = () => {\n      const anchor = anchorRef.current;\n      const floating = floatingRef.current;\n      if (!open || !anchor || !floating) return;\n      const revision = ++version.current;\n      const currentCursor = followCursor ? cursor.current : null;\n      const reference: Element | VirtualElement =\n        currentCursor || pointX !== undefined || pointY !== undefined\n          ? {\n              contextElement: anchor,\n              getBoundingClientRect: () => {\n                const rect = anchor.getBoundingClientRect();\n                const x = currentCursor?.x ?? rect.left + rect.width * (pointX ?? 0.5);\n                const y = currentCursor?.y ?? rect.top + rect.height * (pointY ?? 0.5);\n                return { x, y, left: x, right: x, top: y, bottom: y, width: 0, height: 0 };\n              },\n            }\n          : anchor;\n      void computePosition(reference, floating, {\n        strategy: \"fixed\",\n        placement: currentCursor ? (cursorSide.current ?? side) : side,\n        middleware: [offset(currentCursor ? 12 : 8), flip({ padding: 8 }), shift({ padding: 8 })],\n      }).then(({ x, y, placement }) => {\n        if (version.current !== revision || !floating.isConnected) return;\n        // Hold the chosen side for this hover session. Crossing a flip threshold\n        // repeatedly must not bounce the surface above and below the pointer.\n        if (currentCursor) cursorSide.current = placement;\n        else cursorSide.current = null;\n        const dpr = window.devicePixelRatio || 1;\n        floating.style.transform = `translate3d(${Math.round(x * dpr) / dpr}px, ${Math.round(y * dpr) / dpr}px, 0)`;\n        const origin = {\n          top: \"center bottom\",\n          bottom: \"center top\",\n          left: \"right center\",\n          right: \"left center\",\n        };\n        floating.style.setProperty(\n          \"--tooltip-origin\",\n          origin[placement.split(\"-\")[0] as TooltipSide],\n        );\n        floating.style.visibility = \"visible\";\n        floating.dataset.placement = placement;\n        onPosition();\n      });\n    };\n    if (open) schedule();\n  }, [\n    open,\n    anchorRef,\n    floatingRef,\n    followCursor,\n    pointX,\n    pointY,\n    side,\n    schedule,\n    onPosition,\n    cursor,\n  ]);\n\n  useLayoutEffect(() => {\n    const anchor = anchorRef.current;\n    const floating = floatingRef.current;\n    if (!open || !anchor || !floating) return;\n    const stop = autoUpdate(anchor, floating, schedule);\n    const onScroll = () => {\n      // A stationary pointer no longer describes the same chart point after scrolling.\n      if (followCursor && cursor.current) onDismiss();\n    };\n    window.addEventListener(\"scroll\", onScroll, true);\n    return () => {\n      stop();\n      window.removeEventListener(\"scroll\", onScroll, true);\n      version.current++;\n      if (frame.current !== null) cancelAnimationFrame(frame.current);\n      frame.current = null;\n    };\n  }, [open, anchorRef, floatingRef, followCursor, onDismiss, schedule, cursor]);\n\n  useLayoutEffect(\n    () => () => {\n      version.current++;\n      if (frame.current !== null) cancelAnimationFrame(frame.current);\n    },\n    [],\n  );\n}\n\n/** Pointer lifetime belongs to the trigger, including the opening delay. */\nexport function useTooltipPointer(\n  anchorRef: RefObject<HTMLElement | SVGElement | null>,\n  followCursor: boolean,\n) {\n  const cursor = useRef<TooltipPoint | null>(null);\n  const onMove = useRef<(() => void) | null>(null);\n  const pointerFocus = useRef(false);\n  useLayoutEffect(() => {\n    const anchor = anchorRef.current;\n    if (!anchor || !followCursor) return;\n    const point = (event: PointerEvent) => {\n      if (event.type === \"pointermove\" && event.pointerType === \"touch\" && !event.buttons) return;\n      cursor.current = { x: event.clientX, y: event.clientY };\n      if (event.type === \"pointerdown\") pointerFocus.current = true;\n      onMove.current?.();\n    };\n    const keyboard = () => {\n      cursor.current = null;\n      pointerFocus.current = false;\n      onMove.current?.();\n    };\n    const focus = () => {\n      if (!pointerFocus.current) cursor.current = null;\n      pointerFocus.current = false;\n      onMove.current?.();\n    };\n    const leave = () => {\n      cursor.current = null;\n    };\n    anchor.addEventListener(\"pointerenter\", point as EventListener, { passive: true });\n    anchor.addEventListener(\"pointermove\", point as EventListener, { passive: true });\n    anchor.addEventListener(\"pointerdown\", point as EventListener, { passive: true });\n    anchor.addEventListener(\"pointerleave\", leave);\n    anchor.addEventListener(\"pointercancel\", leave);\n    anchor.addEventListener(\"keydown\", keyboard);\n    anchor.addEventListener(\"focusin\", focus);\n    return () => {\n      cursor.current = null;\n      anchor.removeEventListener(\"pointerenter\", point as EventListener);\n      anchor.removeEventListener(\"pointermove\", point as EventListener);\n      anchor.removeEventListener(\"pointerdown\", point as EventListener);\n      anchor.removeEventListener(\"pointerleave\", leave);\n      anchor.removeEventListener(\"pointercancel\", leave);\n      anchor.removeEventListener(\"keydown\", keyboard);\n      anchor.removeEventListener(\"focusin\", focus);\n    };\n  }, [anchorRef, followCursor]);\n\n  return { cursor, onMove };\n}\n"},{"path":"components/motion/tooltip-surface.tsx","type":"registry:component","target":"@components/motion/tooltip-surface.tsx","content":"\"use client\";\n\nimport { motion, useReducedMotion } from \"motion/react\";\nimport type { ComponentProps, ReactNode, Ref } from \"react\";\nimport { EASE_OUT } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\n/** Presentation only: the unanimated parent owns measurement and positioning. */\nexport function TooltipSurface({\n  children,\n  side: _side = \"top\",\n  className,\n  ref,\n  ready = true,\n  style,\n  ...props\n}: Omit<ComponentProps<typeof motion.span>, \"children\"> & {\n  children?: ReactNode;\n  /** Start the entrance only after the positioning layer has been measured. */\n  ready?: boolean;\n  side?: \"top\" | \"right\" | \"bottom\" | \"left\";\n  ref?: Ref<HTMLSpanElement>;\n}) {\n  const reduce = useReducedMotion();\n  const closed = { opacity: 0, scale: reduce ? 1 : 0.94 };\n  return (\n    <motion.span\n      ref={ref}\n      role=\"tooltip\"\n      initial={closed}\n      animate={{\n        ...(ready ? { opacity: 1, scale: 1 } : closed),\n        transition: { duration: 0.18, ease: EASE_OUT },\n      }}\n      exit={{ ...closed, transition: { duration: 0.12, ease: EASE_OUT } }}\n      style={{ transformOrigin: \"var(--tooltip-origin, center)\", ...style }}\n      className={cn(\n        \"block whitespace-nowrap rounded-lg border border-border bg-background px-2.5 py-1 text-xs font-medium text-foreground shadow-lg\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </motion.span>\n  );\n}\n"},{"path":"lib/hooks/use-dismiss.ts","type":"registry:hook","target":"@lib/hooks/use-dismiss.ts","content":"\"use client\";\n\nimport { type RefObject, useEffect } from \"react\";\n\n/**\n * What the dismissing gesture does to the control it landed on.\n *\n * `\"pass-through\"` is the platform norm (native popover light-dismiss): the\n * tap closes the overlay *and* activates whatever was under it. Use\n * `\"consume\"` where the open overlay sits over or beside controls that would\n * be costly to trigger by accident — the dismissal then swallows the\n * activation too, so the gesture only closes.\n */\nexport type DismissBehavior = \"pass-through\" | \"consume\";\n\nexport interface DismissOptions {\n  /** Default `\"pass-through\"`. */\n  behavior?: DismissBehavior;\n  /** Dismiss on Escape as well. Default true. */\n  escape?: boolean;\n  /** Return true for an outside target that should *not* dismiss. Must be stable. */\n  ignore?: (target: Element) => boolean;\n}\n\n/**\n * What every currently open dismiss scope counts as inside itself. A consumed\n * dismissal reads this to tell a stray gesture from one that belongs to an\n * overlay in front of it: overlays have no shared z-order to consult, but the\n * one the gesture landed in has said as much by registering it.\n */\nconst openScopes = new Set<(target: Element) => boolean>();\n\nfunction claimedByAnotherScope(\n  self: (target: Element) => boolean,\n  target: Element,\n) {\n  for (const scope of openScopes) {\n    if (scope !== self && scope(target)) return true;\n  }\n  return false;\n}\n\n// preventDefault on pointerdown does not suppress the click that follows, so\n// consuming a gesture means swallowing that click itself. The swallower\n// deliberately outlives the effect that installed it — the dismissal it\n// belongs to has already unmounted or re-rendered by the time the click lands.\n// It releases on that click, or on the next gesture if the pointer is dragged\n// away and no click ever arrives, so it can never eat a later one. A keydown\n// releases it too: a gesture that ends with neither a click nor a cancel would\n// otherwise leave it armed, and the click Enter synthesizes on some focused\n// control is not the one this dismissal was owed.\nfunction consumeActivation(source: Event) {\n  const swallow = (event: MouseEvent) => {\n    event.preventDefault();\n    event.stopPropagation();\n    release();\n  };\n  const restart = (event: Event) => {\n    if (event !== source) release();\n  };\n  const release = () => {\n    window.removeEventListener(\"click\", swallow, true);\n    window.removeEventListener(\"pointerdown\", restart, true);\n    window.removeEventListener(\"pointercancel\", restart, true);\n    window.removeEventListener(\"keydown\", release, true);\n  };\n  window.addEventListener(\"click\", swallow, true);\n  window.addEventListener(\"pointerdown\", restart, true);\n  window.addEventListener(\"pointercancel\", restart, true);\n  window.addEventListener(\"keydown\", release, true);\n}\n\n/**\n * Close an open overlay on Escape or a pointerdown outside `ref`. Pass `null`\n * for `ref` when what counts as inside isn't one element, and say so with\n * `ignore` instead.\n *\n * The pointerdown listener is capture-phase: a bubble-phase one is blinded by\n * any handler in between that stops propagation, and an overlay cannot know\n * what it is layered over. `onDismiss` and `ignore` must be stable (wrap in\n * useCallback) so the listeners aren't re-bound every render while open.\n */\nexport function useDismiss(\n  open: boolean,\n  onDismiss: () => void,\n  ref: RefObject<HTMLElement | SVGElement | null> | null,\n  {\n    behavior = \"pass-through\",\n    escape: dismissOnEscape = true,\n    ignore,\n  }: DismissOptions = {},\n) {\n  useEffect(() => {\n    if (!open) return;\n    const inside = (target: Element) =>\n      Boolean(ref?.current?.contains(target)) || Boolean(ignore?.(target));\n    const onKey = (event: KeyboardEvent) => {\n      if (dismissOnEscape && event.key === \"Escape\") onDismiss();\n    };\n    const onPointer = (event: PointerEvent) => {\n      const target = event.target as Element | null;\n      if (!target || inside(target)) return;\n      // Outside this overlay, but inside one that is also open: the gesture is\n      // that overlay's to answer, and swallowing its click from behind would\n      // cost the user the control they actually aimed at.\n      if (behavior === \"consume\" && !claimedByAnotherScope(inside, target)) {\n        consumeActivation(event);\n      }\n      onDismiss();\n    };\n    openScopes.add(inside);\n    window.addEventListener(\"keydown\", onKey);\n    window.addEventListener(\"pointerdown\", onPointer, true);\n    return () => {\n      openScopes.delete(inside);\n      window.removeEventListener(\"keydown\", onKey);\n      window.removeEventListener(\"pointerdown\", onPointer, true);\n    };\n  }, [open, onDismiss, ref, behavior, dismissOnEscape, ignore]);\n}\n"},{"path":"lib/hooks/use-hover-gesture.ts","type":"registry:hook","target":"@lib/hooks/use-hover-gesture.ts","content":"\"use client\";\n\nimport { useMemo, useRef } from \"react\";\nimport { isHoveringPointer } from \"@/lib/touch\";\n\ninterface BoundaryEvent {\n  pointerId: number;\n  pointerType: string;\n  buttons: number;\n}\n\nexport interface HoverGesture {\n  /** True when this enter starts a hover: the pointer arrived resting, not pressing. */\n  enter: (event: BoundaryEvent) => boolean;\n  /** True when this leave ends a hover that entered as one. */\n  leave: (event: BoundaryEvent) => boolean;\n}\n\n/**\n * Pairs a surface's enter with its leave, per pointer.\n *\n * `isHoveringPointer` answers the question the *enter* asks — is this pointer\n * resting on the surface or pressing it — and both boundary cases go wrong if\n * the leave is asked the same question again:\n *\n * - A pen with no hover never rests. It arrives in contact, taps, and the spec\n *   then requires its boundary events after `pointerup`, so the leave carries\n *   `buttons: 0` and reads as a mouse gliding off. Hover teardown then undid\n *   the tap — the panel the pen had just opened closed under it.\n * - A mouse pressed on the surface and dragged off leaves with `buttons: 1`.\n *   Skipping teardown there strands the surface open: the release happens\n *   outside, and no second leave ever comes.\n *\n * So the state a hover holds is released by the pointer that took it, whatever\n * the buttons say at the boundary, and a pointer that arrived in contact never\n * took it in the first place. Contact is the exception tracked here, not\n * hover: a leave from a pointer this surface never saw enter — mounted under\n * the cursor, say — still counts, since the alternative is state with no way\n * out.\n */\nexport function useHoverGesture(): HoverGesture {\n  const contact = useRef(new Set<number>());\n\n  return useMemo(\n    () => ({\n      enter: (event) => {\n        if (isHoveringPointer(event)) {\n          contact.current.delete(event.pointerId);\n          return true;\n        }\n        contact.current.add(event.pointerId);\n        return false;\n      },\n      leave: (event) => {\n        const arrivedInContact = contact.current.delete(event.pointerId);\n        return !arrivedInContact && event.pointerType !== \"touch\";\n      },\n    }),\n    [],\n  );\n}\n"},{"path":"lib/hooks/use-tap-gesture.ts","type":"registry:hook","target":"@lib/hooks/use-tap-gesture.ts","content":"\"use client\";\n\nimport { useMemo, useRef } from \"react\";\n\n/** What a pointerdown recorded, read back by the click that ends its gesture. */\nexport interface TapRecord<S> {\n  /** Which input started the gesture. */\n  pointerType: string;\n  /** What the surface was showing when it started. */\n  state: S;\n}\n\nexport interface TapGesture<S> {\n  /** Record the gesture a pointerdown starts, with the state it starts in. */\n  start: (event: { pointerType: string }, state: S) => void;\n  /** Read the record and clear it. `null` when no pointer is behind this click. */\n  take: () => TapRecord<S> | null;\n  /** Drop the record: this gesture will never spend it on a click. */\n  drop: () => void;\n}\n\n/**\n * The pointer gesture behind a click, recorded where the click cannot report\n * it. A `click` carries no `pointerType` in the engines that matter, so the\n * `pointerdown` before it is the only thing that says which input activated\n * the control — and whether one did at all, since keyboard activation\n * synthesizes a click with no pointer behind it.\n *\n * State goes in with the record because a click reports that no better: a\n * browser that focuses a control on contact can open the very panel the tap\n * was meant to open, and reading \"is it open\" at click time then undoes it.\n * What the gesture started against is what it acts on.\n *\n * The record is spent by one click and dropped by everything else, because a\n * record that outlives its gesture is worse than none:\n *\n * - A scroll or an OS gesture takes the touch away — `pointercancel`, no click\n *   ever — and the finger would sit in the record until some later click.\n * - That later click is often `Enter` on a keyboard, which arrives with no\n *   pointerdown of its own and would inherit the abandoned finger. A keydown\n *   is the start of a keyboard activation and never part of a tap, so it drops\n *   the record too.\n *\n * Both ends have to be wired by the surface: `drop` on `onPointerCancel` and\n * on `onKeyDown`.\n */\nexport function useTapGesture<S>(): TapGesture<S> {\n  const record = useRef<TapRecord<S> | null>(null);\n\n  return useMemo(\n    () => ({\n      start: (event, state) => {\n        record.current = { pointerType: event.pointerType, state };\n      },\n      take: () => {\n        const spent = record.current;\n        record.current = null;\n        return spent;\n      },\n      drop: () => {\n        record.current = null;\n      },\n    }),\n    [],\n  );\n}\n"},{"path":"lib/touch.ts","type":"registry:lib","target":"@lib/touch.ts","content":"// Shared touch primitives. iOS and iPadOS run their own gestures on top of the\n// page — the long-press selection callout and the selection it drags in with\n// it — and they win: once the platform claims a touch it cancels ours\n// mid-gesture, so a press-and-hold or a drag simply dies. Surfaces that own\n// their gesture have to opt out.\n//\n// What the two classes below cover, precisely:\n// - `-webkit-touch-callout: none` stops iOS's long-press callout. WebKit-only:\n//   it is not a property other engines have, so it is inert everywhere else.\n// - `user-select: none` stops the long-press selection on every engine,\n//   Android included, and stops a drag from painting a selection under the\n//   cursor. It is inherited, so it reaches every descendant — which is why the\n//   two classes differ only in whether they apply it unconditionally.\n// What neither covers:\n// - Chrome for Android's long-press menu on a link or an image. No CSS\n//   suppresses it; a gesture surface that wraps one needs its own\n//   `onContextMenu` with `preventDefault()`.\n// - The native drag of an `<img>` or `<a>` descendant. `-webkit-user-drag` is\n//   not inherited and plain divs and buttons are not drag sources, so setting\n//   it on the surface does nothing — the child itself needs `draggable={false}`.\n\n/**\n * Classes for a surface that *is* the control: a thumb, a drum, a stage, a\n * handle, a hold button. Selection is suppressed on every input, because a\n * drag that highlights the control's own label is wrong on a mouse too.\n * Compose with `touch-none` when the surface also owns the scroll axis — leave\n * it off when the page must still scroll from there.\n */\nexport const TOUCH_GESTURE_CLASS = \"select-none [-webkit-touch-callout:none]\";\n\n/**\n * The same opt-out for a gesture surface that wraps content the consumer owns:\n * a scroller, a context-menu trigger, a sheet header, a list row. Selection is\n * suppressed only where the platform runs its own press gestures — a coarse\n * pointer — so a mouse user can still select and copy that content. If the\n * gesture itself would paint a selection under the cursor, add `select-none`\n * for the duration of the gesture rather than reaching for\n * `TOUCH_GESTURE_CLASS`.\n *\n * `pointer: coarse` describes the *primary* pointer and nothing else, so a\n * hybrid machine reads it wrong in both directions: a tablet with a mouse\n * plugged in keeps touch as primary and loses mouse selection, and a laptop\n * with a touchscreen keeps the mouse as primary and leaves selection live\n * under a finger. No media query can answer per interaction — the query is\n * about the device, and the question is about the gesture in progress. The\n * default stays here because it is right on the machines that are one thing or\n * the other, and losing a selection is a nuisance; where the miss costs a\n * *gesture* instead, the surface pairs it with `holdSelection` on the press.\n */\nexport const TOUCH_GESTURE_CONTENT_CLASS =\n  \"[-webkit-touch-callout:none] pointer-coarse:select-none\";\n\n/**\n * Suppress selection on `element` for as long as a gesture is running on it,\n * whatever the primary pointer of the machine happens to be. Returns the\n * release. Inline, so it wins over the class above and is gone again the\n * moment the gesture ends.\n *\n * For the press gestures a native selection would otherwise steal — a\n * long-press that opens a menu. Elsewhere prefer the classes: a surface that\n * takes selection away for the whole session is a surface whose text nobody\n * can copy.\n */\nexport function holdSelection(element: HTMLElement) {\n  element.style.setProperty(\"user-select\", \"none\");\n  element.style.setProperty(\"-webkit-user-select\", \"none\");\n  return () => {\n    element.style.removeProperty(\"user-select\");\n    element.style.removeProperty(\"-webkit-user-select\");\n  };\n}\n\n/**\n * Pointer capture, best effort. WebKit throws `NotFoundError` when the pointer\n * is already gone by the time the handler runs — routine on iOS, where the\n * system can claim the touch first — and an uncaught throw takes the rest of\n * the handler, the gesture included, down with it. Touch pointers carry\n * implicit capture anyway, so losing it is never fatal.\n */\nexport function capturePointer(element: Element, pointerId: number) {\n  try {\n    element.setPointerCapture(pointerId);\n  } catch {\n    // Pointer is no longer active — implicit capture still applies on touch.\n  }\n}\n\n/** Release a capture taken with `capturePointer`, ignoring a stale pointer. */\nexport function releasePointer(element: Element, pointerId: number) {\n  try {\n    if (element.hasPointerCapture(pointerId)) {\n      element.releasePointerCapture(pointerId);\n    }\n  } catch {\n    // Capture was already dropped by the browser.\n  }\n}\n\n/**\n * Whether this event came from a pointer that is *hovering*: not a touch, and\n * not currently pressed. Which input the user is holding right now is not\n * something a device capability can answer — a touchscreen laptop hovers and\n * taps, and iPadOS reports a fine hovering pointer for a finger — so both\n * paths stay live and each handler branches on the event it was given.\n *\n * A pen resting on the glass is making contact, not hovering: `buttons` is the\n * tell, and it sends a pen tap down the same route a finger takes.\n *\n * This answers what an *enter* asks. A leave is the other half of a pair and\n * has to be read against the enter that started it — `useHoverGesture` in\n * `lib/hooks/use-hover-gesture` does that, and hover surfaces should use it\n * rather than asking this question twice.\n */\nexport const isHoveringPointer = (event: {\n  pointerType: string;\n  buttons: number;\n}) => event.pointerType !== \"touch\" && event.buttons === 0;\n"}]}