{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "treemap-chart",
  "type": "registry:ui",
  "title": "Treemap chart",
  "description": "A squarified SVG treemap with one level of grouping, labels that hide in small cells, hover and keyboard focus with a value tooltip, and a hidden data table.",
  "dependencies": [],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/ui/treemap-chart.tsx",
      "type": "registry:ui",
      "target": "components/ui/treemap-chart.tsx",
      "content": "\"use client\"\nimport * as React from \"react\"\nimport { cn } from \"@/lib/utils\"\n\n/** A squarified SVG treemap with one level of grouping, labels that hide in small cells, hover and keyboard focus with a value tooltip, and a hidden data table. */\ntype TreemapLeaf = {\n  label: string\n  value: number\n  /** Any CSS colour. Defaults to the group's chart token. */\n  color?: string\n}\n\ntype TreemapGroup = {\n  label: string\n  /** Any CSS colour. Defaults to the chart tokens in order. */\n  color?: string\n  children: TreemapLeaf[]\n}\n\ntype TreemapNode = TreemapLeaf | TreemapGroup\n\ntype TreemapChartProps = {\n  /** Top level items. A node with `children` renders as a group with a header strip. */\n  data?: TreemapNode[]\n  /** Heading above the chart and the accessible name. */\n  title?: string\n  /** Shown next to the total. */\n  caption?: string\n  /** Chart height in px. The width follows the container. */\n  height?: number\n  /** Formats values in the tooltip, labels and table. */\n  format?: (value: number) => string\n  /** Hide labels in cells narrower or shorter than this, in px. */\n  minLabelSize?: number\n  className?: string\n}\n\ntype Rect = { x: number; y: number; w: number; h: number }\ntype Cell = { key: string; label: string; group: string | null; value: number; color: string; strength: number; rect: Rect; share: number; groupShare: number }\ntype GroupBox = { label: string; value: number; color: string; rect: Rect; header: number }\n\nconst SAMPLE: TreemapNode[] = [\n  {\n    label: \"Payments\",\n    children: [\n      { label: \"Enterprise\", value: 48200 },\n      { label: \"Scale\", value: 31600 },\n      { label: \"Growth\", value: 22400 },\n      { label: \"Starter\", value: 9800 },\n    ],\n  },\n  {\n    label: \"Invoicing\",\n    children: [\n      { label: \"Enterprise\", value: 26400 },\n      { label: \"Scale\", value: 18900 },\n      { label: \"Growth\", value: 14200 },\n      { label: \"Starter\", value: 6100 },\n    ],\n  },\n  {\n    label: \"Banking\",\n    children: [\n      { label: \"Scale\", value: 12600 },\n      { label: \"Growth\", value: 9400 },\n      { label: \"Enterprise\", value: 8100 },\n    ],\n  },\n  {\n    label: \"Expenses\",\n    children: [\n      { label: \"Growth\", value: 7300 },\n      { label: \"Scale\", value: 5200 },\n      { label: \"Starter\", value: 2400 },\n    ],\n  },\n  { label: \"Add-ons\", value: 4100 },\n]\n\nconst COLORS = [\"var(--chart-1)\", \"var(--chart-2)\", \"var(--chart-3)\", \"var(--chart-4)\", \"var(--chart-5)\"]\nconst STRENGTH = [74, 60, 48, 38, 30, 24, 20]\nconst HEADER = 22\nconst PAD = 3\nconst usd = new Intl.NumberFormat(\"en-US\", { style: \"currency\", currency: \"USD\", notation: \"compact\", maximumFractionDigits: 1 })\nconst defaultFormat = (n: number) => usd.format(n)\n\nconst isGroup = (n: TreemapNode): n is TreemapGroup => Array.isArray((n as TreemapGroup).children)\nconst nodeValue = (n: TreemapNode) => (isGroup(n) ? n.children.reduce((s, c) => s + Math.max(0, c.value), 0) : Math.max(0, n.value))\n\n/** Squarified treemap (Bruls, Huizing, van Wijk): rows along the shorter side, keeping cells close to square. */\nfunction squarify(values: number[], rect: Rect): Rect[] {\n  const out: Rect[] = values.map(() => ({ x: rect.x, y: rect.y, w: 0, h: 0 }))\n  const total = values.reduce((s, v) => s + Math.max(0, v), 0)\n  if (total <= 0 || rect.w <= 0 || rect.h <= 0) return out\n  const scale = (rect.w * rect.h) / total\n  const items = values\n    .map((v, i) => ({ a: Math.max(0, v) * scale, i }))\n    .filter((it) => it.a > 0)\n    .sort((p, q) => q.a - p.a)\n  let { x, y, w, h } = rect\n  let row: typeof items = []\n  const worst = (r: typeof items, len: number) => {\n    let s = 0\n    let mx = 0\n    let mn = Infinity\n    for (const it of r) {\n      s += it.a\n      mx = Math.max(mx, it.a)\n      mn = Math.min(mn, it.a)\n    }\n    return Math.max((len * len * mx) / (s * s), (s * s) / (len * len * mn))\n  }\n  const layout = (r: typeof items) => {\n    const s = r.reduce((a, it) => a + it.a, 0)\n    if (w >= h) {\n      const cw = Math.min(w, s / h)\n      let cy = y\n      for (const it of r) {\n        const ch = it.a / cw\n        out[it.i] = { x, y: cy, w: cw, h: ch }\n        cy += ch\n      }\n      x += cw\n      w -= cw\n    } else {\n      const ch = Math.min(h, s / w)\n      let cx = x\n      for (const it of r) {\n        const cw = it.a / ch\n        out[it.i] = { x: cx, y, w: cw, h: ch }\n        cx += cw\n      }\n      y += ch\n      h -= ch\n    }\n  }\n  for (const it of items) {\n    const len = Math.min(w, h)\n    if (!row.length || len <= 0 || worst([...row, it], len) <= worst(row, len)) row.push(it)\n    else {\n      layout(row)\n      row = [it]\n    }\n  }\n  if (row.length) layout(row)\n  return out\n}\n\nfunction fit(text: string, width: number, px: number) {\n  const max = Math.floor(width / (px * 0.56))\n  if (max < 2) return \"\"\n  return text.length <= max ? text : text.slice(0, Math.max(1, max - 1)).trimEnd() + \"…\"\n}\n\n/**\n * A squarified treemap. Groups get a header strip and their cells share one\n * hue at falling strengths, labels appear only where they fit, and hovering\n * or focusing a cell lifts it and shows its value and share in a tooltip.\n * Cells are keyboard focusable with arrow keys; a hidden table carries the data.\n */\nfunction TreemapChart({\n  data = SAMPLE,\n  title = \"Revenue by product line\",\n  caption = \"October, by plan\",\n  height = 320,\n  format = defaultFormat,\n  minLabelSize = 44,\n  className,\n}: TreemapChartProps) {\n  const boxRef = React.useRef<HTMLDivElement>(null)\n  const refs = React.useRef<(SVGRectElement | null)[]>([])\n  const [width, setWidth] = React.useState(640)\n  const [active, setActive] = React.useState<number | null>(null)\n  const [focusIdx, setFocusIdx] = React.useState(0)\n  const titleId = React.useId()\n\n  React.useEffect(() => {\n    const el = boxRef.current\n    if (!el || typeof ResizeObserver === \"undefined\") return\n    const ro = new ResizeObserver(([e]) => setWidth(Math.max(120, Math.round(e.contentRect.width))))\n    ro.observe(el)\n    return () => ro.disconnect()\n  }, [])\n\n  const total = data.reduce((s, n) => s + nodeValue(n), 0)\n\n  const { cells, groups } = React.useMemo(() => {\n    const cells: Cell[] = []\n    const groups: GroupBox[] = []\n    const top = squarify(data.map(nodeValue), { x: 0, y: 0, w: width, h: height })\n    data.forEach((n, gi) => {\n      const rect = top[gi]\n      const color = n.color ?? COLORS[gi % COLORS.length]\n      const value = nodeValue(n)\n      if (!isGroup(n)) {\n        cells.push({ key: n.label, label: n.label, group: null, value, color, strength: STRENGTH[0], rect, share: total ? value / total : 0, groupShare: 1 })\n        return\n      }\n      const header = rect.h >= HEADER * 2 && rect.w >= 40 ? HEADER : 0\n      groups.push({ label: n.label, value, color, rect, header })\n      const inner = { x: rect.x + PAD, y: rect.y + header + PAD, w: Math.max(0, rect.w - PAD * 2), h: Math.max(0, rect.h - header - PAD * 2) }\n      const order = n.children.map((c, i) => ({ c, i })).sort((a, b) => b.c.value - a.c.value)\n      const rank = new Map(order.map((o, r) => [o.i, r]))\n      const kids = squarify(n.children.map((c) => Math.max(0, c.value)), inner)\n      n.children.forEach((c, ci) => {\n        const v = Math.max(0, c.value)\n        cells.push({\n          key: `${n.label}/${c.label}`,\n          label: c.label,\n          group: n.label,\n          value: v,\n          color: c.color ?? color,\n          strength: STRENGTH[Math.min(STRENGTH.length - 1, rank.get(ci) ?? 0)],\n          rect: kids[ci],\n          share: total ? v / total : 0,\n          groupShare: value ? v / value : 0,\n        })\n      })\n    })\n    return { cells, groups }\n  }, [data, width, height, total])\n\n  const move = (to: number) => {\n    if (!cells.length) return\n    const n = (to + cells.length) % cells.length\n    setFocusIdx(n)\n    refs.current[n]?.focus()\n  }\n\n  const pct = (n: number) => `${(n * 100).toFixed(n < 0.1 ? 1 : 0)}%`\n  const cur = active != null ? cells[active] : null\n  const tipLeft = cur ? Math.max(84, Math.min(width - 84, cur.rect.x + cur.rect.w / 2)) : 0\n  // Above the cell when there is room, otherwise just inside its top edge.\n  const tipAbove = cur ? cur.rect.y >= 100 : true\n  const tipTop = cur ? (tipAbove ? cur.rect.y - 6 : cur.rect.y + 10) : 0\n\n  return (\n    <figure\n      data-slot=\"treemap-chart\"\n      aria-labelledby={titleId}\n      className={cn(\"w-full max-w-2xl rounded-xl border border-border bg-surface p-5 shadow-raised\", className)}\n    >\n      <figcaption className=\"flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1\">\n        <p id={titleId} className=\"text-[0.8125rem] font-medium text-fg\">\n          {title}\n        </p>\n        <p className=\"text-xs text-fg-muted\">\n          <span className=\"font-medium text-fg tabular-nums\">{format(total)}</span>\n          {caption ? <span> · {caption}</span> : null}\n        </p>\n      </figcaption>\n\n      <div ref={boxRef} className=\"relative mt-4 w-full\" style={{ height }}>\n        <svg\n          viewBox={`0 0 ${width} ${height}`}\n          width=\"100%\"\n          height={height}\n          preserveAspectRatio=\"none\"\n          role=\"group\"\n          aria-label={`${title}, ${cells.length} cells`}\n          className=\"block overflow-visible\"\n          onPointerLeave={() => setActive(null)}\n        >\n          {groups.map((g) => (\n            <g key={g.label} aria-hidden>\n              <rect\n                x={g.rect.x}\n                y={g.rect.y}\n                width={Math.max(0, g.rect.w)}\n                height={Math.max(0, g.rect.h)}\n                rx={6}\n                stroke=\"var(--surface)\"\n                strokeWidth={2}\n                style={{ fill: `color-mix(in oklch, ${g.color} 10%, var(--surface))` }}\n              />\n              {g.header ? (\n                <text x={g.rect.x + 8} y={g.rect.y + 15} className=\"fill-fg\" style={{ fontSize: 11, fontWeight: 500 }}>\n                  {fit(g.label, g.rect.w - 16, 11)}\n                  {g.rect.w >= 150 ? (\n                    <tspan className=\"fill-fg-muted\" style={{ fontWeight: 400 }} dx={6}>\n                      {format(g.value)}\n                    </tspan>\n                  ) : null}\n                </text>\n              ) : null}\n            </g>\n          ))}\n          {cells.map((c, i) => {\n            const on = active === i\n            const dim = active != null && !on\n            const { x, y, w, h } = c.rect\n            if (w <= 0 || h <= 0) return null\n            const showLabel = w >= minLabelSize && h >= 26\n            const showValue = showLabel && h >= 44\n            return (\n              <g key={c.key} className=\"transition-opacity duration-[140ms] ease-hairline\" style={{ opacity: dim ? 0.55 : 1 }}>\n                <rect\n                  ref={(el) => {\n                    refs.current[i] = el\n                  }}\n                  x={x}\n                  y={y}\n                  width={w}\n                  height={h}\n                  rx={4}\n                  role=\"img\"\n                  aria-label={`${c.group ? `${c.group}, ` : \"\"}${c.label}: ${format(c.value)}, ${pct(c.share)} of total`}\n                  tabIndex={i === focusIdx ? 0 : -1}\n                  stroke={on ? \"var(--fg)\" : \"var(--surface)\"}\n                  strokeWidth={on ? 1.5 : 2}\n                  style={{ fill: `color-mix(in oklch, ${c.color} ${c.strength}%, var(--surface))` }}\n                  className=\"cursor-pointer outline-none\"\n                  onPointerEnter={() => setActive(i)}\n                  onFocus={() => {\n                    setFocusIdx(i)\n                    setActive(i)\n                  }}\n                  onBlur={() => setActive(null)}\n                  onKeyDown={(e) => {\n                    if (e.key === \"ArrowRight\" || e.key === \"ArrowDown\") { e.preventDefault(); move(i + 1) }\n                    if (e.key === \"ArrowLeft\" || e.key === \"ArrowUp\") { e.preventDefault(); move(i - 1) }\n                    if (e.key === \"Home\") { e.preventDefault(); move(0) }\n                    if (e.key === \"End\") { e.preventDefault(); move(cells.length - 1) }\n                  }}\n                />\n                {showLabel ? (\n                  <text x={x + 7} y={y + 16} className=\"pointer-events-none fill-fg\" style={{ fontSize: 11, fontWeight: 500 }} aria-hidden>\n                    {fit(c.label, w - 14, 11)}\n                  </text>\n                ) : null}\n                {showValue ? (\n                  <text x={x + 7} y={y + 31} className=\"pointer-events-none fill-fg-muted tabular-nums\" style={{ fontSize: 11 }} aria-hidden>\n                    {fit(format(c.value), w - 14, 11)}\n                  </text>\n                ) : null}\n              </g>\n            )\n          })}\n        </svg>\n\n        {cur ? (\n          <div\n            role=\"status\"\n            data-slot=\"treemap-chart-tooltip\"\n            className={cn(\n              \"pointer-events-none absolute z-10 min-w-40 -translate-x-1/2 rounded-lg border border-border bg-raised p-2.5 shadow-lg\",\n              tipAbove ? \"-translate-y-full\" : \"\"\n            )}\n            style={{ left: tipLeft, top: tipTop }}\n          >\n            <p className=\"flex items-center gap-1.5 text-xs font-medium text-fg\">\n              <span aria-hidden className=\"size-2 shrink-0 rounded-full\" style={{ background: cur.color }} />\n              {cur.group ? `${cur.group} · ${cur.label}` : cur.label}\n            </p>\n            <ul className=\"mt-1.5 space-y-1 text-xs\">\n              <li className=\"flex justify-between gap-4\">\n                <span className=\"text-fg-muted\">Revenue</span>\n                <span className=\"text-fg tabular-nums\">{format(cur.value)}</span>\n              </li>\n              <li className=\"flex justify-between gap-4\">\n                <span className=\"text-fg-muted\">Of total</span>\n                <span className=\"text-fg tabular-nums\">{pct(cur.share)}</span>\n              </li>\n              {cur.group ? (\n                <li className=\"flex justify-between gap-4\">\n                  <span className=\"text-fg-muted\">Of {cur.group}</span>\n                  <span className=\"text-fg tabular-nums\">{pct(cur.groupShare)}</span>\n                </li>\n              ) : null}\n            </ul>\n          </div>\n        ) : null}\n      </div>\n\n      <table className=\"sr-only\">\n        <caption>{title}</caption>\n        <thead>\n          <tr>\n            <th scope=\"col\">Group</th>\n            <th scope=\"col\">Item</th>\n            <th scope=\"col\">Value</th>\n            <th scope=\"col\">Share</th>\n          </tr>\n        </thead>\n        <tbody>\n          {cells.map((c) => (\n            <tr key={c.key}>\n              <td>{c.group ?? \"\"}</td>\n              <th scope=\"row\">{c.label}</th>\n              <td>{format(c.value)}</td>\n              <td>{pct(c.share)}</td>\n            </tr>\n          ))}\n        </tbody>\n        <tfoot>\n          <tr>\n            <th scope=\"row\" colSpan={2}>\n              Total\n            </th>\n            <td>{format(total)}</td>\n            <td>100%</td>\n          </tr>\n        </tfoot>\n      </table>\n    </figure>\n  )\n}\n\nexport { TreemapChart }\nexport type { TreemapChartProps, TreemapNode, TreemapLeaf, TreemapGroup }\n"
    }
  ]
}
