{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "bubble-chart",
  "title": "Bubble chart",
  "description": "A bubble chart sized by value: on two axes when items have x and y, packed largest-first otherwise, with direct labels where they fit, one tooltip and one Tab stop with arrow keys.",
  "registryDependencies": [
    "utils",
    "@manniche/chart-kit"
  ],
  "files": [
    {
      "path": "registry/manniche/bubble-chart/bubble-chart.tsx",
      "content": "// Bubble chart: one circle per item, sized by its value. Give every item an `x` and a `y` and the bubbles sit on\n// two axes (how often, how much each time) with dashed gridlines and values at the right; leave them out and the\n// bubbles pack together, largest in the middle. Each bubble is named by a label beside it (on axes) or inside it\n// (packed) wherever the label fits; the rest are named by the tooltip.\n//\n// How it works: the layout is worked out in pixels from the measured size, so labels, radii and ticks stay readable\n// from 280 to 1200 px. Labels on axes try eight spots around their bubble and keep the one that hits nothing; packed\n// bubbles are placed largest first, each in the free spot nearest the middle. The plot is one Tab stop: arrow keys,\n// Home and End step through the bubbles (left to right on axes, largest first when packed), Escape lets go. Hover\n// and a finger scrubbing across the plot pick the bubble under it and show the same tooltip, and the page still\n// scrolls up and down under a finger.\n//\n// Screen readers: the plot is an image named by `label` with a short hint; each step is read once as one sentence\n// (\"Harbour Market: 6 visits, average €48.20, total €289.20\"), and the whole data set is in a hidden table.\n//\n// Motion: on first view the bubbles grow from their centres one after another (all done in 600 ms). Bubbles only\n// move by transform and fade by opacity, so new data glides them to their new places. Under reduced motion\n// everything is drawn in place at once and nothing moves.\nimport { useCallback, useEffect, useId, useMemo, useState, type HTMLAttributes, type KeyboardEvent, type PointerEvent } from 'react'\nimport { cn } from '@/lib/utils'\nimport { ChartTooltip, SrTable } from '@/registry/manniche/chart-kit/chart-kit'\nimport {\n  BASE,\n  EASE_CSS,\n  GRID,\n  formatTick,\n  formatValue,\n  niceScale,\n  seriesColor,\n  stepIndex,\n  valueParts,\n  type ValueFormat,\n} from '@/registry/manniche/chart-kit/chart-utils'\nimport { useAnnounce, useChartFrame } from '@/registry/manniche/chart-kit/use-chart'\n\nexport type BubbleDatum = {\n  /** The item's name, written beside or inside its bubble and read aloud. Also its key, so keep it unique. */\n  label: string\n  /** Sets the bubble's area. Negative values count as zero. */\n  value: number\n  /** Position along the bottom axis. Give every item an `x` and a `y` to draw axes; leave both out to pack. */\n  x?: number\n  /** Position up the side axis. */\n  y?: number\n  /** A group name, shown in the tooltip and the table, such as \"Groceries\". Items in one group share a colour. */\n  group?: string\n  /** Any CSS colour. Defaults to a series colour per group (or per item without groups). */\n  color?: string\n}\n\nexport type BubbleLabels = {\n  /** The first column of the hidden table. Default \"Item\". */\n  item?: string\n  /** What `value` measures, in the table and the default sentence. Default \"Value\". */\n  value?: string\n  /** Muted words after the value in the tooltip, such as \"this month\". Default none. */\n  valueNote?: string\n  /** What `x` measures, in the tooltip, the table and the default sentence. Default \"X\". */\n  x?: string\n  /** What `y` measures. Default \"Y\". */\n  y?: string\n  /** A unit after the last bottom tick, such as \"visits\". Default none. */\n  xUnit?: string\n  /** The group column of the hidden table. Default \"Group\". */\n  group?: string\n  /** Read once when the plot gets focus. Default \"Arrow keys step through the bubbles; Escape lets go.\" */\n  hint?: string\n  /** The sentence read for a bubble. Gets the item and its formatted figures. */\n  describe?: (d: BubbleDatum, text: { value: string; x: string; y: string }) => string\n}\n\nexport type BubbleChartProps = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {\n  /** One entry per bubble: `{ label, value, x?, y?, group?, color? }`. */\n  data: BubbleDatum[]\n  /** What the chart shows, such as \"Merchants by visits and average spend\". Names the plot and the hidden table. */\n  label: string\n  /** How `value` is written. Default: a plain whole number in en-GB. */\n  format?: ValueFormat\n  /** How `x` is written. Default: a plain number. */\n  xFormat?: ValueFormat\n  /** How `y` is written, on the side axis too. Default: a plain number. */\n  yFormat?: ValueFormat\n  /** The plot's height in px. Default 280 below 560 px wide and 320 above (240/280 in compact density). */\n  height?: number\n  /** The largest bubble's radius in px. Default 40 (30 below 560 px wide, 22 below 400), and never more than an eighth of the height. */\n  maxRadius?: number\n  /** The bubble that is active (its tooltip shows), as an index into `data`, or null. Makes the selection controlled. */\n  activeIndex?: number | null\n  /** The bubble active on first render when uncontrolled. Default null. */\n  defaultActiveIndex?: number | null\n  /** Called with the new index (or null) when the pointer or the keyboard moves the selection. */\n  onActiveIndexChange?: (index: number | null) => void\n  /** Words for i18n. */\n  labels?: BubbleLabels\n  /** \"compact\" makes the plot shorter. Inside a DataTile it follows the tile's density on its own. */\n  density?: 'comfortable' | 'compact'\n}\n\ntype Placed = { i: number; cx: number; cy: number; r: number; color: string }\ntype Tag = { x: number; y: number; anchor: 'start' | 'middle' | 'end'; inside: boolean } | null\n\nconst GUT = 46 // right gutter for the side axis values\nconst PB = 30 // bottom room for the x ticks\nconst PT = 14\nconst LABEL_H = 14\n\n/** A label's width in px at 12 px, guessed from its letters so layout is the same on the server and the client. */\nfunction textWidth(s: string, px = 12) {\n  let em = 0\n  for (const ch of s) {\n    if (/[il.,'|!:;ɪj]/.test(ch)) em += 0.27\n    else if (ch === ' ') em += 0.27\n    else if (/[mwMW@]/.test(ch)) em += 0.86\n    else if (/[A-Z&]/.test(ch)) em += 0.66\n    else if (/[ft r]/.test(ch)) em += 0.36\n    else em += 0.56\n  }\n  return em * px\n}\n\nfunction useControllable(controlled: number | null | undefined, initial: number | null, onChange?: (v: number | null) => void) {\n  const [inner, setInner] = useState(initial)\n  const isControlled = controlled !== undefined\n  const value = isControlled ? controlled : inner\n  const set = useCallback(\n    (v: number | null) => {\n      if (v === value) return\n      if (!isControlled) setInner(v)\n      onChange?.(v)\n    },\n    [value, isControlled, onChange],\n  )\n  return [value, set] as const\n}\n\n/** Packs circles (radii in any unit) largest first, each in the free tangent spot nearest the middle. */\nfunction pack(radii: number[], aspect: number) {\n  const order = radii.map((_, i) => i).sort((a, b) => radii[b] - radii[a])\n  const pos: { x: number; y: number; r: number }[] = new Array(radii.length)\n  const done: number[] = []\n  const pad = Math.max(...radii, 0) * 0.06\n  const free = (x: number, y: number, r: number) =>\n    done.every((j) => (pos[j].x - x) ** 2 + (pos[j].y - y) ** 2 >= (pos[j].r + r + pad - 1e-6) ** 2)\n  // Wider plots spread sideways: distance counts less along x.\n  const cost = (x: number, y: number) => (x / aspect) ** 2 + y ** 2\n  for (const i of order) {\n    const r = radii[i]\n    if (!done.length) {\n      pos[i] = { x: 0, y: 0, r }\n      done.push(i)\n      continue\n    }\n    let best: { x: number; y: number } | null = null\n    let bc = Infinity\n    const tryAt = (x: number, y: number) => {\n      const c = cost(x, y)\n      if (c < bc && free(x, y, r)) {\n        bc = c\n        best = { x, y }\n      }\n    }\n    for (const a of done) {\n      const A = pos[a]\n      for (let k = 0; k < 12; k++) {\n        const t = (k / 12) * Math.PI * 2\n        tryAt(A.x + Math.cos(t) * (A.r + r + pad), A.y + Math.sin(t) * (A.r + r + pad))\n      }\n      for (const b of done) {\n        if (b <= a) continue\n        const B = pos[b]\n        const ra = A.r + r + pad\n        const rb = B.r + r + pad\n        const dx = B.x - A.x\n        const dy = B.y - A.y\n        const d = Math.hypot(dx, dy)\n        if (d > ra + rb || d < Math.abs(ra - rb) || d === 0) continue\n        const l = (ra * ra - rb * rb + d * d) / (2 * d)\n        const h = Math.sqrt(Math.max(0, ra * ra - l * l))\n        const mx = A.x + (dx * l) / d\n        const my = A.y + (dy * l) / d\n        tryAt(mx + (h * dy) / d, my - (h * dx) / d)\n        tryAt(mx - (h * dy) / d, my + (h * dx) / d)\n      }\n    }\n    pos[i] = best ? { ...(best as { x: number; y: number }), r } : { x: 0, y: 0, r }\n    done.push(i)\n  }\n  return pos\n}\n\n/**\n * A bubble chart: bubbles sized by value, on two axes when the data has `x` and `y`, packed together when it does\n * not. Direct labels where they fit, one tooltip for the rest, one Tab stop with arrow keys.\n */\nexport function BubbleChart({\n  data,\n  label,\n  format,\n  xFormat,\n  yFormat,\n  height,\n  maxRadius,\n  activeIndex,\n  defaultActiveIndex = null,\n  onActiveIndexChange,\n  labels: labelsProp,\n  density,\n  className,\n  style,\n  ...rest\n}: BubbleChartProps) {\n  const labels = {\n    item: 'Item',\n    value: 'Value',\n    x: 'X',\n    y: 'Y',\n    group: 'Group',\n    hint: 'Arrow keys step through the bubbles; Escape lets go.',\n    ...labelsProp,\n  }\n  const { ref: frameRef, width: frameW, height: frameH, drawn, reduced } = useChartFrame<HTMLDivElement>()\n  const { say, region } = useAnnounce()\n  const hintId = useId()\n  const [active, setActive] = useControllable(activeIndex, defaultActiveIndex, onActiveIndexChange)\n  const [keyboard, setKeyboard] = useState(false)\n\n  const n = data.length\n  const axes = n > 0 && data.every((d) => Number.isFinite(d.x) && Number.isFinite(d.y))\n  const W = Math.max(280, frameW)\n  const H = frameH\n  const ready = frameW > 0 && H > 0\n\n  // Colours: one per group, in the order groups first appear; otherwise one per item.\n  const groups = useMemo(() => [...new Set(data.map((d) => d.group).filter((g): g is string => !!g))], [data])\n  const colorOf = useCallback(\n    (d: BubbleDatum, i: number) => d.color ?? (d.group ? seriesColor(groups.indexOf(d.group)) : seriesColor(i)),\n    [groups],\n  )\n\n  const layout = useMemo(() => {\n    if (!ready || !n) return null\n    const maxV = Math.max(...data.map((d) => Math.max(0, d.value)), Number.EPSILON)\n    if (axes) {\n      const pw = W - GUT\n      // The axes start at 0, or lower when a value is negative.\n      const xv = data.map((d) => d.x as number)\n      const yv = data.map((d) => d.y as number)\n      const xs = niceScale(Math.max(...xv), 4, Math.min(0, ...xv))\n      const ys = niceScale(Math.max(...yv), 4, Math.min(0, ...yv))\n      const rmax = Math.min(maxRadius ?? (W < 400 ? 22 : W < 560 ? 30 : 40), H / 8)\n      const rmin = Math.min(6, rmax / 2)\n      const X = (v: number) => 16 + ((v - xs.min) / (xs.max - xs.min || 1)) * (pw - 32)\n      const Y = (v: number) => PT + (1 - (v - ys.min) / (ys.max - ys.min || 1)) * (H - PT - PB)\n      const dots: Placed[] = data.map((d, i) => ({\n        i,\n        cx: X(d.x as number),\n        cy: Y(d.y as number),\n        r: rmin + Math.sqrt(Math.max(0, d.value) / maxV) * (rmax - rmin),\n        color: colorOf(d, i),\n      }))\n      // Direct labels: eight spots around each bubble, biggest bubbles first; the cheapest spot wins.\n      type Box = { x: number; y: number; w: number; h: number }\n      const placed: Box[] = []\n      const hitsCircle = (b: Box, o: Placed) => {\n        const nx = Math.max(b.x, Math.min(o.cx, b.x + b.w))\n        const ny = Math.max(b.y, Math.min(o.cy, b.y + b.h))\n        return (nx - o.cx) ** 2 + (ny - o.cy) ** 2 < (o.r + 2) ** 2\n      }\n      const hitsBox = (a: Box, b: Box) => a.x < b.x + b.w + 4 && b.x < a.x + a.w + 4 && a.y < b.y + b.h + 2 && b.y < a.y + a.h + 2\n      const tags: Tag[] = new Array(n).fill(null)\n      for (const o of [...dots].sort((p, q) => q.r - p.r)) {\n        const w = textWidth(data[o.i].label)\n        const h = LABEL_H\n        const d = o.r * 0.7\n        const spots: (Box & { a: 'start' | 'middle' | 'end' })[] = [\n          { x: o.cx + o.r + 7, y: o.cy - h / 2, a: 'start', w, h },\n          { x: o.cx - o.r - 7 - w, y: o.cy - h / 2, a: 'end', w, h },\n          { x: o.cx - w / 2, y: o.cy - o.r - 6 - h, a: 'middle', w, h },\n          { x: o.cx - w / 2, y: o.cy + o.r + 6, a: 'middle', w, h },\n          { x: o.cx + d + 3, y: o.cy - d - h, a: 'start', w, h },\n          { x: o.cx - d - 3 - w, y: o.cy - d - h, a: 'end', w, h },\n          { x: o.cx + d + 3, y: o.cy + d, a: 'start', w, h },\n          { x: o.cx - d - 3 - w, y: o.cy + d, a: 'end', w, h },\n        ]\n        const cost = (b: Box) =>\n          (b.x < 0 || b.x + b.w > pw || b.y < 0 || b.y + b.h > H - PB ? 10 : 0) +\n          dots.filter((q) => q !== o && hitsCircle(b, q)).length * 3 +\n          placed.filter((q) => hitsBox(b, q)).length * 4\n        let best = spots[0]\n        let bc = Infinity\n        for (const s of spots) {\n          const c = cost(s)\n          if (c < bc) {\n            bc = c\n            best = s\n          }\n          if (!c) break\n        }\n        // A label that would hit something is left to the tooltip.\n        if (bc > 0) continue\n        placed.push(best)\n        const ax = best.a === 'start' ? best.x : best.a === 'end' ? best.x + w : best.x + w / 2\n        tags[o.i] = { x: ax, y: best.y + 11, anchor: best.a, inside: false }\n      }\n      const xTicks = xs.ticks.map((v) => ({ v, at: X(v) }))\n      const yTicks = ys.ticks.map((v) => ({ v, at: Y(v) }))\n      // Left to right, then bottom to top: the order the eye reads the plot.\n      const order = dots\n        .map((o) => o.i)\n        .sort((a, b) => (data[a].x as number) - (data[b].x as number) || (data[a].y as number) - (data[b].y as number))\n      return { dots, tags, order, pw, xTicks, yTicks }\n    }\n    // Packed: unit radii by area, packed, then scaled to fit the plot with room for the focus ring.\n    const radii = data.map((d) => Math.sqrt(Math.max(0, d.value) / maxV))\n    const pos = pack(radii, Math.max(1, W / H))\n    let x0 = Infinity\n    let x1 = -Infinity\n    let y0 = Infinity\n    let y1 = -Infinity\n    for (const p of pos) {\n      x0 = Math.min(x0, p.x - p.r)\n      x1 = Math.max(x1, p.x + p.r)\n      y0 = Math.min(y0, p.y - p.r)\n      y1 = Math.max(y1, p.y + p.r)\n    }\n    const m = 8\n    let s = Math.min((W - 2 * m) / (x1 - x0 || 1), (H - 2 * m) / (y1 - y0 || 1))\n    if (maxRadius) s = Math.min(s, maxRadius)\n    const ox = W / 2 - ((x0 + x1) / 2) * s\n    const oy = H / 2 - ((y0 + y1) / 2) * s\n    const dots: Placed[] = data.map((d, i) => ({ i, cx: ox + pos[i].x * s, cy: oy + pos[i].y * s, r: pos[i].r * s, color: colorOf(d, i) }))\n    const tags: Tag[] = dots.map((o) => (textWidth(data[o.i].label) <= o.r * 1.7 - 8 && o.r >= 20 ? { x: o.cx, y: o.cy + 4, anchor: 'middle', inside: true } : null))\n    const order = dots.map((o) => o.i).sort((a, b) => data[b].value - data[a].value || a - b)\n    return { dots, tags, order, pw: W, xTicks: [], yTicks: [] }\n  }, [ready, n, data, axes, W, H, maxRadius, colorOf])\n\n  // First view: the bubbles grow in once; afterwards moves glide without a delay.\n  const [settled, setSettled] = useState(false)\n  useEffect(() => {\n    if (!drawn || settled) return\n    const t = window.setTimeout(() => setSettled(true), 650)\n    return () => window.clearTimeout(t)\n  }, [drawn, settled])\n  const grown = drawn && ready\n\n  // A bubble that no longer exists lets go.\n  useEffect(() => {\n    if (active !== null && (active < 0 || active >= n)) setActive(null)\n  }, [active, n, setActive])\n\n  const at = active !== null && active >= 0 && active < n ? active : -1\n  const order = useMemo(() => layout?.order ?? [], [layout])\n\n  const texts = (d: BubbleDatum) => ({\n    value: formatValue(d.value, format),\n    x: d.x === undefined ? '' : formatValue(d.x, xFormat),\n    y: d.y === undefined ? '' : formatValue(d.y, yFormat),\n  })\n  const describe = (i: number) => {\n    const d = data[i]\n    const t = texts(d)\n    if (labels.describe) return labels.describe(d, t)\n    const parts = [`${labels.value} ${t.value}`]\n    if (axes) parts.unshift(`${labels.x} ${t.x}`, `${labels.y} ${t.y}`)\n    return `${d.label}${d.group ? `, ${d.group}` : ''}: ${parts.join(', ')}`\n  }\n  const pick = (i: number, announce: boolean) => {\n    setActive(i)\n    if (announce) say(describe(i))\n  }\n\n  const fromPointer = (e: PointerEvent<HTMLDivElement>) => {\n    if (!layout) return\n    setKeyboard(false)\n    const r = e.currentTarget.getBoundingClientRect()\n    const px = e.clientX - r.left\n    const py = e.clientY - r.top\n    // The bubble under the pointer, smallest first so a small one in front of a big one can be reached; a finger\n    // gets 10 px of slack.\n    const slack = e.pointerType === 'touch' ? 10 : 2\n    let hit = -1\n    let hr = Infinity\n    for (const o of layout.dots) {\n      if ((o.cx - px) ** 2 + (o.cy - py) ** 2 <= (o.r + slack) ** 2 && o.r < hr) {\n        hit = o.i\n        hr = o.r\n      }\n    }\n    if (hit >= 0) {\n      if (hit !== at) pick(hit, false)\n    } else if (e.pointerType === 'mouse' && !e.currentTarget.matches(':focus-visible')) setActive(null)\n  }\n\n  const onKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {\n    if (!n || !order.length) return\n    if (e.key === 'Escape') {\n      if (at < 0) return\n      e.preventDefault()\n      setActive(null)\n      return\n    }\n    const p = order.indexOf(at)\n    const to = p < 0 && /^Arrow/.test(e.key) ? 0 : stepIndex(e.key, p, order.length, { loop: false })\n    if (to === null) return\n    e.preventDefault()\n    setKeyboard(true)\n    pick(order[to], true)\n  }\n\n  const isCompact = density === 'compact'\n  const heightClass = isCompact\n    ? 'h-[240px] @[35rem]:h-[280px]'\n    : 'h-[280px] @[35rem]:h-[320px] group-data-[density=compact]/tile:h-[240px] @[35rem]:group-data-[density=compact]/tile:h-[280px]'\n\n  const stagger = Math.min(50, 300 / Math.max(1, n - 1))\n  const motionFor = (k: number) =>\n    reduced\n      ? { transition: 'none' }\n      : settled\n        ? { transitionProperty: 'transform, opacity', transitionDuration: '300ms, 200ms', transitionTimingFunction: EASE_CSS }\n        : {\n            transitionProperty: 'transform, opacity',\n            transitionDuration: '300ms, 200ms',\n            transitionTimingFunction: EASE_CSS,\n            transitionDelay: `${Math.round(k * stagger)}ms, 0ms`,\n          }\n  const rankOf = useMemo(() => {\n    const m = new Map<number, number>()\n    order.forEach((i, k) => m.set(i, k))\n    return m\n  }, [order])\n\n  const tip = at >= 0 && layout ? layout.dots[at] : null\n  const tipD = at >= 0 ? data[at] : null\n  const tipRows = tipD && axes ? [\n    { label: labels.x, value: texts(tipD).x },\n    { label: labels.y, value: texts(tipD).y },\n  ] : []\n\n  return (\n    <div className={cn('@container relative min-w-0', className)} style={style} {...rest}>\n      <div\n        ref={frameRef}\n        tabIndex={n ? 0 : undefined}\n        role=\"img\"\n        aria-label={label}\n        aria-describedby={n ? hintId : undefined}\n        onPointerDown={fromPointer}\n        onPointerMove={fromPointer}\n        onPointerLeave={(e) => {\n          if (e.pointerType !== 'mouse') return\n          if (!e.currentTarget.matches(':focus-visible')) setActive(null)\n        }}\n        onFocus={(e) => {\n          // A press focuses the plot too; only keyboard focus picks the first bubble.\n          if (!e.currentTarget.matches(':focus-visible')) return\n          setKeyboard(true)\n          if (at >= 0) say(describe(at))\n          else if (order.length) pick(order[0], true)\n        }}\n        onBlur={() => {\n          setKeyboard(false)\n          setActive(null)\n        }}\n        onKeyDown={onKeyDown}\n        className={cn('relative w-full rounded-xl outline-offset-4 select-none [touch-action:pan-y]', heightClass)}\n        style={height ? { height } : undefined}\n      >\n        {layout && (\n          <svg width={W} height={H} viewBox={`0 0 ${W} ${H}`} className=\"absolute inset-0 block overflow-visible\" aria-hidden>\n            {axes && (\n              <g>\n                {layout.yTicks.map((t, k) => (\n                  <g key={`y${t.v}`}>\n                    <line\n                      x1={0}\n                      x2={layout.pw}\n                      y1={Math.round(t.at) + 0.5}\n                      y2={Math.round(t.at) + 0.5}\n                      stroke={k === 0 ? BASE : GRID}\n                      strokeDasharray={k === 0 ? undefined : '2 4'}\n                    />\n                    <text x={layout.pw + 10} y={t.at + 4} className=\"fill-muted-foreground font-mono text-[11px] tabular-nums\">\n                      {formatTick(t.v, yFormat)}\n                    </text>\n                  </g>\n                ))}\n                {layout.xTicks.map((t, k) => (\n                  <text\n                    key={`x${t.v}`}\n                    x={t.at}\n                    y={H - 8}\n                    textAnchor=\"middle\"\n                    className=\"fill-muted-foreground font-mono text-[11px] tabular-nums\"\n                  >\n                    {formatTick(t.v, xFormat)}\n                    {k === layout.xTicks.length - 1 && labels.xUnit ? ` ${labels.xUnit}` : ''}\n                  </text>\n                ))}\n              </g>\n            )}\n            {layout.dots.map((o) => {\n              const k = rankOf.get(o.i) ?? o.i\n              const dim = at >= 0 && at !== o.i\n              return (\n                <circle\n                  key={data[o.i].label}\n                  cx={0}\n                  cy={0}\n                  r={o.r}\n                  fill={axes ? o.color : `color-mix(in oklab, ${o.color} 30%, var(--card))`}\n                  stroke={axes ? 'var(--card)' : o.color}\n                  strokeWidth={axes ? 2.5 : 1.5}\n                  style={{\n                    transform: `translate(${o.cx}px, ${o.cy}px) scale(${grown ? 1 : 0})`,\n                    opacity: dim ? 0.28 : 1,\n                    ...motionFor(k),\n                  }}\n                />\n              )\n            })}\n            {layout.dots.map((o) => {\n              const t = layout.tags[o.i]\n              if (!t) return null\n              const k = rankOf.get(o.i) ?? o.i\n              const dim = at >= 0 && at !== o.i\n              return (\n                <text\n                  key={data[o.i].label}\n                  x={0}\n                  y={0}\n                  textAnchor={t.anchor}\n                  className=\"fill-foreground text-[12px] font-medium\"\n                  style={{\n                    transform: `translate(${t.x}px, ${t.y}px)`,\n                    opacity: grown ? (dim ? 0.35 : 1) : 0,\n                    ...motionFor(k),\n                  }}\n                >\n                  {data[o.i].label}\n                </text>\n              )\n            })}\n            {tip && (\n              <circle\n                cx={0}\n                cy={0}\n                r={tip.r + 4}\n                fill=\"none\"\n                stroke=\"var(--ring)\"\n                strokeWidth={2}\n                style={{ transform: `translate(${tip.cx}px, ${tip.cy}px)`, opacity: keyboard ? 1 : 0, transition: 'none' }}\n              />\n            )}\n          </svg>\n        )}\n        <ChartTooltip\n          open={!!tip}\n          x={tip ? tip.cx : 0}\n          y={tip ? (tip.cy - tip.r < 70 ? tip.cy + tip.r : tip.cy - tip.r) : 0}\n          below={tip ? tip.cy - tip.r < 70 : false}\n          bounds={W}\n          title={tipD ? (tipD.group ? `${tipD.label} · ${tipD.group}` : tipD.label) : ''}\n          value={tipD ? <TipValue value={tipD.value} format={format} note={labels.valueNote} /> : ''}\n          rows={tipRows}\n        />\n      </div>\n      <span id={hintId} hidden>\n        {labels.hint}\n      </span>\n      <SrTable\n        caption={label}\n        head={[labels.item, ...(groups.length ? [labels.group] : []), ...(axes ? [labels.x, labels.y] : []), labels.value]}\n        rows={data.map((d) => {\n          const t = texts(d)\n          return [d.label, ...(groups.length ? [d.group ?? ''] : []), ...(axes ? [t.x, t.y] : []), t.value]\n        })}\n      />\n      {region}\n    </div>\n  )\n}\n\n/** The tooltip figure with its unit and note set muted. */\nfunction TipValue({ value, format, note }: { value: number; format?: ValueFormat; note?: string }) {\n  const p = valueParts(value, format)\n  const muted = 'font-medium text-[color-mix(in_oklab,var(--card)_60%,var(--foreground))]'\n  return (\n    <>\n      {p.sign}\n      {!p.unitAfter && <span className={muted}>{p.unit}</span>}\n      {p.whole}\n      {p.fraction}\n      {p.unitAfter && <span className={muted}> {p.unit}</span>}\n      {p.suffix}\n      {note && <span className={cn(muted, 'text-[13px] font-normal')}> {note}</span>}\n    </>\n  )\n}\n\nexport default BubbleChart\n",
      "type": "registry:component"
    }
  ],
  "categories": [
    "widgets"
  ],
  "type": "registry:component"
}