{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "chart",
  "title": "Chart",
  "description": "Chart from the Get IT Done design system.",
  "dependencies": [
    "lucide-react@^0.379.0"
  ],
  "registryDependencies": [
    "https://zero-design.itdone.de/r/data-state-panel.json",
    "https://zero-design.itdone.de/r/labels-context.json",
    "https://zero-design.itdone.de/r/recipes.json",
    "https://zero-design.itdone.de/r/utils.json",
    "https://zero-design.itdone.de/r/theme.json"
  ],
  "files": [
    {
      "path": "registry/ui/chart.tsx",
      "content": "'use client';\n\nimport { BarChart3 } from 'lucide-react';\nimport { forwardRef, useCallback, useEffect, useId, useLayoutEffect, useRef, useState, type HTMLAttributes, type ReactNode, type RefObject } from 'react';\n\nimport { overlaySurface } from '@/lib/recipes';\nimport { cn } from '@/lib/utils';\nimport { DataStatePanel, SkeletonBar } from '@/components/ui/data-state-panel';\nimport { useLabels, useLocale, usePreset, type DesignSystemLocale } from '@/components/ui/labels-context';\n\n// Tones\n\n// Full class strings so Tailwind's content scan finds every chart token.\nconst toneText = {\n  1: 'text-chart-1',\n  2: 'text-chart-2',\n  3: 'text-chart-3',\n  4: 'text-chart-4',\n  5: 'text-chart-5',\n  6: 'text-chart-6',\n  7: 'text-chart-7',\n  8: 'text-chart-8',\n  brand: 'text-chart-brand',\n  'brand-1': 'text-chart-brand-1',\n  'brand-2': 'text-chart-brand-2',\n  'brand-3': 'text-chart-brand-3',\n  'brand-4': 'text-chart-brand-4',\n  'brand-5': 'text-chart-brand-5',\n  'brand-6': 'text-chart-brand-6',\n  'brand-7': 'text-chart-brand-7',\n  neutral: 'text-chart-neutral',\n  success: 'text-chart-success',\n  destructive: 'text-chart-destructive',\n  warning: 'text-text-warning',\n} as const;\n\nconst toneStroke = {\n  1: 'stroke-chart-1',\n  2: 'stroke-chart-2',\n  3: 'stroke-chart-3',\n  4: 'stroke-chart-4',\n  5: 'stroke-chart-5',\n  6: 'stroke-chart-6',\n  7: 'stroke-chart-7',\n  8: 'stroke-chart-8',\n  brand: 'stroke-chart-brand',\n  'brand-1': 'stroke-chart-brand-1',\n  'brand-2': 'stroke-chart-brand-2',\n  'brand-3': 'stroke-chart-brand-3',\n  'brand-4': 'stroke-chart-brand-4',\n  'brand-5': 'stroke-chart-brand-5',\n  'brand-6': 'stroke-chart-brand-6',\n  'brand-7': 'stroke-chart-brand-7',\n  neutral: 'stroke-chart-neutral',\n  success: 'stroke-chart-success',\n  destructive: 'stroke-chart-destructive',\n  warning: 'stroke-text-warning',\n} as const;\n\nconst toneBg = {\n  1: 'bg-chart-1',\n  2: 'bg-chart-2',\n  3: 'bg-chart-3',\n  4: 'bg-chart-4',\n  5: 'bg-chart-5',\n  6: 'bg-chart-6',\n  7: 'bg-chart-7',\n  8: 'bg-chart-8',\n  brand: 'bg-chart-brand',\n  'brand-1': 'bg-chart-brand-1',\n  'brand-2': 'bg-chart-brand-2',\n  'brand-3': 'bg-chart-brand-3',\n  'brand-4': 'bg-chart-brand-4',\n  'brand-5': 'bg-chart-brand-5',\n  'brand-6': 'bg-chart-brand-6',\n  'brand-7': 'bg-chart-brand-7',\n  neutral: 'bg-chart-neutral',\n  success: 'bg-chart-success',\n  destructive: 'bg-chart-destructive',\n  warning: 'bg-chart-warning',\n} as const;\n\n// Label colour for text drawn on a chart fill (the `-on` roles of the chart tokens), keyed like toneBg.\nconst toneOn = {\n  1: 'text-chart-1-on',\n  2: 'text-chart-2-on',\n  3: 'text-chart-3-on',\n  4: 'text-chart-4-on',\n  5: 'text-chart-5-on',\n  6: 'text-chart-6-on',\n  7: 'text-chart-7-on',\n  8: 'text-chart-8-on',\n  brand: 'text-chart-brand-on',\n  'brand-1': 'text-chart-brand-1-on',\n  'brand-2': 'text-chart-brand-2-on',\n  'brand-3': 'text-chart-brand-3-on',\n  'brand-4': 'text-chart-brand-4-on',\n  'brand-5': 'text-chart-brand-5-on',\n  'brand-6': 'text-chart-brand-6-on',\n  'brand-7': 'text-chart-brand-7-on',\n  neutral: 'text-chart-neutral-on',\n  success: 'text-chart-success-on',\n  destructive: 'text-chart-destructive-on',\n  warning: 'text-chart-warning-on',\n} as const;\n\n// Label colour for a custom `bg-*` fill: the project tiles have their own `-on` role, anything else keeps the on-brand label.\nconst customOn: Record<string, string> = {\n  'bg-data-project-1': 'text-data-project-1-on',\n  'bg-data-project-2': 'text-data-project-2-on',\n  'bg-data-project-3': 'text-data-project-3-on',\n  'bg-data-project-4': 'text-data-project-4-on',\n  'bg-data-project-5': 'text-data-project-5-on',\n  'bg-data-project-6': 'text-data-project-6-on',\n  'bg-data-project-7': 'text-data-project-7-on',\n  'bg-data-project-8': 'text-data-project-8-on',\n};\n\nexport type ChartTone = keyof typeof toneText;\n\n// Corner radius of the zero bars, the `borderRadius/tight` token (4). SVG geometry cannot read the CSS variable.\nconst BAR_TOP_RADIUS = 4;\n\n// Rectangle with only the top corners rounded.\nconst topRoundedPath = (x: number, y: number, w: number, h: number, radius: number) => {\n  const r = Math.max(0, Math.min(radius, w / 2, h));\n  return `M${x},${y + h}V${y + r}Q${x},${y} ${x + r},${y}H${x + w - r}Q${x + w},${y} ${x + w},${y + r}V${y + h}Z`;\n};\n\nconst cycle: ChartTone[] = [1, 2, 3, 4, 5, 6, 7, 8];\nconst toneAt = (i: number): ChartTone => cycle[i % cycle.length];\n\n// Legend\n\nexport interface ChartLegendItem {\n  id: string;\n  label: ReactNode;\n  tone?: ChartTone;\n  // Custom swatch colour (user data): a CSS colour such as `var(--colors-data-project-3)` or a token utility such as `bg-data-project-3`. Wins over `tone`.\n  color?: string;\n}\n\nexport interface ChartLegendProps extends HTMLAttributes<HTMLUListElement> {\n  items: ChartLegendItem[];\n  // Direction of the list: a row (top, bottom) or a column (right).\n  layout?: 'row' | 'column';\n}\n\nconst ChartLegend = forwardRef<HTMLUListElement, ChartLegendProps>(({ className, items, layout = 'row', ...props }, ref) => {\n  const l = useLabels();\n  const zero = usePreset() === 'zero';\n  const swatch = zero ? 'h-swatch-sm w-swatch-sm rounded-hairline' : 'h-2 w-2 rounded-full';\n  return (\n  <ul\n    ref={ref}\n    aria-label={l.legend}\n    className={cn('typography-text-small m-0 flex list-none p-0 text-text-subtle', layout === 'row' ? 'flex-row flex-wrap items-center gap-4' : 'flex-col gap-1', className)}\n    {...props}\n  >\n    {items.map((item, i) => (\n      <li key={item.id} className=\"flex items-center gap-2\">\n        {item.color ? (\n          <span\n            aria-hidden=\"true\"\n            data-legend-swatch=\"\"\n            className={cn('shrink-0', swatch, item.color.startsWith('bg-') ? item.color : 'bg-current')}\n            style={item.color.startsWith('bg-') ? undefined : { backgroundColor: item.color }}\n          />\n        ) : (\n          <span aria-hidden=\"true\" className={cn('shrink-0', swatch, toneBg[item.tone ?? toneAt(i)])} />\n        )}\n        <span>{item.label}</span>\n      </li>\n    ))}\n  </ul>\n  );\n});\nChartLegend.displayName = 'ChartLegend';\n\n// Frame (name, description, table alternative, states)\n\nexport interface ChartTable {\n  caption?: string;\n  columns: string[];\n  rows: (string | number)[][];\n}\n\nexport interface ChartFrameProps extends Omit<HTMLAttributes<HTMLElement>, 'title'> {\n  // Accessible name of the chart.\n  title: string;\n  // Short text summary, read after the name.\n  description?: string;\n  // Data table alternative. Rendered visually hidden unless `showTable` is set.\n  table?: ChartTable;\n  showTable?: boolean;\n  legend?: ChartLegendItem[];\n  legendPosition?: 'top' | 'right' | 'bottom';\n  state?: 'default' | 'loading' | 'empty' | 'error';\n  emptyTitle?: string;\n  emptyDescription?: string;\n  errorTitle?: string;\n  errorDescription?: string;\n  onRetry?: () => void;\n  loadingLabel?: string;\n}\n\nconst ChartFrame = forwardRef<HTMLElement, ChartFrameProps>(\n  (\n    {\n      className,\n      title,\n      description,\n      table,\n      showTable = false,\n      legend,\n      legendPosition = 'bottom',\n      state = 'default',\n      emptyTitle: emptyTitleProp,\n      emptyDescription,\n      errorTitle: errorTitleProp,\n      errorDescription: errorDescriptionProp,\n      onRetry,\n      loadingLabel: loadingLabelProp,\n      children,\n      ...props\n    },\n    ref,\n  ) => {\n    const l = useLabels();\n    const emptyTitle = emptyTitleProp ?? l.chartEmptyTitle;\n    const errorTitle = errorTitleProp ?? l.chartErrorTitle;\n    const errorDescription = errorDescriptionProp ?? l.chartErrorDescription;\n    const loadingLabel = loadingLabelProp ?? l.chartLoading;\n    const id = useId();\n    const descId = description ? `${id}-desc` : undefined;\n    const tableMarkup = table ? (\n      <table className={cn('typography-text-extra-small w-full text-left')}>\n        {table.caption ? <caption className={cn(showTable && 'pb-2 text-left text-muted-foreground')}>{table.caption}</caption> : null}\n        <thead>\n          <tr>\n            {table.columns.map((c) => (\n              <th key={c} scope=\"col\" className=\"py-1 pr-3 font-medium\">\n                {c}\n              </th>\n            ))}\n          </tr>\n        </thead>\n        <tbody>\n          {table.rows.map((row, r) => (\n            <tr key={r}>\n              {row.map((cell, c) =>\n                c === 0 ? (\n                  <th key={c} scope=\"row\" className=\"py-1 pr-3 font-normal\">\n                    {cell}\n                  </th>\n                ) : (\n                  <td key={c} className=\"py-1 pr-3 tabular-nums\">\n                    {cell}\n                  </td>\n                ),\n              )}\n            </tr>\n          ))}\n        </tbody>\n      </table>\n    ) : null;\n    // A bare `sr-only` table keeps its table layout and widens the page. The hidden variant sits in a clipped block, the visible one scrolls on its own.\n    const tableNode = tableMarkup ? (\n      showTable ? <div className=\"max-w-full overflow-x-auto\">{tableMarkup}</div> : <div className=\"sr-only\">{tableMarkup}</div>\n    ) : null;\n\n    const legendNode =\n      legend && legend.length > 0 ? <ChartLegend items={legend} layout={legendPosition === 'right' ? 'column' : 'row'} /> : null;\n\n    return (\n      <figure ref={ref} aria-busy={state === 'loading' || undefined} className={cn('relative m-0 flex min-w-0 flex-col gap-3', className)} {...props}>\n        {state === 'default' ? (\n          <>\n            {legendPosition === 'top' ? legendNode : null}\n            <div className={cn('flex min-w-0 gap-4', legendPosition === 'right' ? 'flex-row items-center' : 'flex-col')}>\n              <div role=\"img\" aria-label={title} aria-describedby={descId} className=\"min-w-0 flex-1\">\n                {children}\n              </div>\n              {legendPosition === 'right' ? legendNode : null}\n            </div>\n            {description ? (\n              <p id={descId} className=\"sr-only\">\n                {description}\n              </p>\n            ) : null}\n            {legendPosition === 'bottom' ? legendNode : null}\n            {tableNode}\n          </>\n        ) : null}\n        {state === 'loading' ? (\n          <div role=\"status\" aria-label={loadingLabel} className=\"flex h-40 items-end gap-3\">\n            {[40, 70, 55, 85, 60, 75, 50].map((h, i) => (\n              <SkeletonBar key={i} className=\"flex-1\" style={{ height: `${h}%` }} />\n            ))}\n          </div>\n        ) : null}\n        {state === 'empty' ? (\n          <DataStatePanel kind=\"empty\" icon={<BarChart3 className=\"h-icon-md w-icon-md\" />} title={emptyTitle} description={emptyDescription} />\n        ) : null}\n        {state === 'error' ? <DataStatePanel kind=\"error\" title={errorTitle} description={errorDescription} onRetry={onRetry} /> : null}\n      </figure>\n    );\n  },\n);\nChartFrame.displayName = 'ChartFrame';\n\ntype FrameShared = Pick<\n  ChartFrameProps,\n  | 'title'\n  | 'description'\n  | 'showTable'\n  | 'state'\n  | 'onRetry'\n  | 'emptyTitle'\n  | 'emptyDescription'\n  | 'errorTitle'\n  | 'errorDescription'\n  | 'loadingLabel'\n  | 'legendPosition'\n  | 'className'\n>;\n\n// Axis helpers\n\nconst niceMax = (max: number) => {\n  if (max <= 0) return 1;\n  const exp = Math.pow(10, Math.floor(Math.log10(max)));\n  const f = max / exp;\n  const nice = f <= 1 ? 1 : f <= 2 ? 2 : f <= 5 ? 5 : 10;\n  return nice * exp;\n};\n\nconst formatNumber = (n: number, locale: DesignSystemLocale) =>\n  new Intl.NumberFormat(locale === 'de' ? 'de-DE' : 'en-US', { maximumFractionDigits: 1 }).format(n);\n\n// Number formatter bound to the provider locale (English by default).\nconst useNumberFormat = () => {\n  const locale = useLocale();\n  return (n: number) => formatNumber(n, locale);\n};\n\n// Line and bar chart\n\nexport interface ChartSeries {\n  id: string;\n  label: string;\n  values: number[];\n  tone?: ChartTone;\n  // Per data point tone for bars (Figma Column chart, one colour per column). A missing entry falls back to the series tone.\n  pointTones?: (ChartTone | undefined)[];\n  // Per data point CSS colour for bars, for user data such as `var(--colors-data-project-3)`. Wins over `pointTones`.\n  pointColors?: (string | undefined)[];\n}\n\n// One category of a LineBarChart as handed to `derived.compute`.\nexport interface ChartDatum {\n  index: number;\n  category: string;\n  // Value of every series at this category, by series id.\n  values: Record<string, number>;\n  // Sum of all series at this category.\n  total: number;\n}\n\n// A value computed from the data of a category, e.g. a conversion rate. It is shown as a tooltip row and as a column of the data table.\nexport interface ChartDerived {\n  key: string;\n  label: string;\n  compute: (datum: ChartDatum) => number | null | undefined;\n  // Formats the computed value. Defaults to the locale number format.\n  format?: (value: number) => string;\n}\n\n// Approximate glyph width of the axis text in SVG units, used to wrap long category labels without measuring the DOM.\nconst AXIS_CHAR_WIDTH = 6.5;\nconst AXIS_LINE_HEIGHT = 14;\n\n// Splits a label into lines of at most `maxChars` characters, breaking at spaces and inside words that are too long. The last line ends with an ellipsis when text is left over.\nconst wrapAxisLabel = (label: string, maxChars: number, maxLines: number): string[] => {\n  const limit = Math.max(1, maxChars);\n  const lines: string[] = [];\n  let line = '';\n  const push = () => {\n    if (line) lines.push(line);\n    line = '';\n  };\n  label\n    .split(/\\s+/)\n    .filter(Boolean)\n    .forEach((word) => {\n      let rest = word;\n      while (rest.length > limit) {\n        if (line) push();\n        lines.push(rest.slice(0, limit));\n        rest = rest.slice(limit);\n      }\n      if (!line) line = rest;\n      else if (line.length + 1 + rest.length <= limit) line = `${line} ${rest}`;\n      else {\n        push();\n        line = rest;\n      }\n    });\n  push();\n  if (lines.length <= maxLines) return lines.length ? lines : [label];\n  const kept = lines.slice(0, maxLines);\n  const last = kept[maxLines - 1];\n  kept[maxLines - 1] = `${last.length >= limit ? last.slice(0, Math.max(0, limit - 1)) : last}\\u2026`;\n  return kept;\n};\n\n// Narrowest a fluid chart renders, so a collapsed container does not produce a negative plot.\nconst FLUID_MIN_WIDTH = 120;\n\n// Pixel width of a container, kept up to date with a ResizeObserver. The node arrives through a callback ref, so a container that mounts later (after a loading or empty state) is measured as soon as it exists. Null while disabled or before the first measurement (and where the DOM has no layout, e.g. jsdom).\nconst useContainerWidth = (enabled: boolean) => {\n  const [node, setNode] = useState<HTMLElement | null>(null);\n  const [measured, setMeasured] = useState<number | null>(null);\n  useLayoutEffect(() => {\n    if (!enabled || !node) {\n      setMeasured(null);\n      return undefined;\n    }\n    const apply = (px: number) => {\n      if (px <= 0) return;\n      const next = Math.max(FLUID_MIN_WIDTH, Math.floor(px));\n      setMeasured((prev) => (prev === next ? prev : next));\n    };\n    apply(node.clientWidth);\n    if (typeof ResizeObserver === 'undefined') return undefined;\n    const observer = new ResizeObserver((entries) => {\n      const entry = entries[entries.length - 1];\n      if (entry) apply(entry.contentRect.width);\n    });\n    observer.observe(node);\n    return () => observer.disconnect();\n  }, [node, enabled]);\n  return [setNode, enabled ? measured : null] as const;\n};\n\nexport interface LineBarChartProps extends FrameShared {\n  type?: 'line' | 'bar';\n  categories: string[];\n  series: ChartSeries[];\n  // Stack bar series (Bar zero style).\n  stacked?: boolean;\n  axisLabels?: boolean;\n  xTitle?: string;\n  yTitle?: string;\n  legend?: boolean;\n  // Chart width in px (the svg still shrinks with its container through the viewBox). 'auto' measures the container with a ResizeObserver and draws at the real pixel width, so the axis text keeps its token size at any width. `width` is the fallback until measured: 'auto' uses 600.\n  width?: number | 'auto';\n  height?: number;\n  // First column header of the data table alternative. Defaults to the localized \"Category\".\n  categoryLabel?: string;\n  // Pointer tooltip with the values of the category under the pointer. Stays inside the chart, Escape hides it. The data table remains the assistive tech alternative.\n  tooltip?: boolean;\n  // Value label above each vertical bar (above the total on stacked bars).\n  showValues?: boolean;\n  // Wraps long category labels onto several lines instead of letting them overlap. The plot gives up the room the extra lines need.\n  wrapLabels?: boolean;\n  // Widest a wrapped label may get, in chart units. Defaults to the width of one category band.\n  labelMaxWidth?: number;\n  // Line limit of a wrapped label. The last line ends with an ellipsis when text is cut. Default 3.\n  labelMaxLines?: number;\n  // Values computed per category, for example a conversion rate. Each one is a row in the tooltip and a column in the data table.\n  derived?: ChartDerived | ChartDerived[];\n}\n\n// Hover tooltip of a LineBarChart. It measures itself and clamps into the chart box so it never overflows.\nconst ChartTooltip = ({ title, rows, anchor, scale, containerRef }: { title: string; rows: { id: string; label: string; value: string; tone?: ChartTone }[]; anchor: number; scale: number; containerRef: RefObject<HTMLDivElement> }) => {\n  const ref = useRef<HTMLDivElement>(null);\n  const [left, setLeft] = useState(0);\n  useLayoutEffect(() => {\n    const box = containerRef.current?.clientWidth ?? 0;\n    const tip = ref.current?.offsetWidth ?? 0;\n    const px = anchor * scale;\n    const gap = 8;\n    const preferred = px + gap + tip > box ? px - gap - tip : px + gap;\n    setLeft(Math.max(0, Math.min(preferred, Math.max(0, box - tip))));\n  }, [anchor, scale, rows.length, title, containerRef]);\n  return (\n    <div\n      ref={ref}\n      data-chart-tooltip=\"\"\n      aria-hidden=\"true\"\n      style={{ left }}\n      className={cn('pointer-events-none absolute top-0 z-raised flex max-w-full flex-col gap-1 px-3 py-2 typography-text-extra-small', overlaySurface)}\n    >\n      <span className=\"truncate font-medium text-text-default\">{title}</span>\n      {rows.map((row) => (\n        <span key={row.id} data-derived={row.tone ? undefined : ''} className={cn('flex items-center gap-2 text-text-subtle', !row.tone && 'border-t border-solid border-border-subtle pt-1')}>\n          {row.tone ? <span aria-hidden=\"true\" className={cn('h-2 w-2 shrink-0 rounded-full', toneBg[row.tone])} /> : null}\n          <span className=\"min-w-0 flex-1 truncate\">{row.label}</span>\n          <span className=\"tabular-nums text-text-default\">{row.value}</span>\n        </span>\n      ))}\n    </div>\n  );\n};\n\nconst LineBarChart = forwardRef<HTMLElement, LineBarChartProps>(\n  (\n    { type = 'bar', categories, series, stacked = false, axisLabels = true, xTitle, yTitle, legend = false, legendPosition = 'bottom', width: widthProp = 600, height = 240, categoryLabel, tooltip = false, showValues: showValuesProp, wrapLabels = false, labelMaxWidth, labelMaxLines = 3, derived, ...frame },\n    ref,\n  ) => {\n    const l = useLabels();\n    const zero = usePreset() === 'zero';\n    // Zero: 11px axis text in the subtle colour, value labels above the bars, bars rounded on top.\n    const axisText = zero ? 'typography-kpi-label fill-current text-text-subtle' : 'typography-text-extra-small fill-current text-muted-foreground';\n    const valueText = zero ? 'typography-kpi-label-strong fill-current text-text-default' : 'typography-text-extra-small fill-current text-muted-foreground';\n    const format = useNumberFormat();\n    const fluid = widthProp === 'auto';\n    const wrapRef = useRef<HTMLDivElement | null>(null);\n    const [measureRef, measuredWidth] = useContainerWidth(fluid);\n    const setWrap = useCallback(\n      (node: HTMLDivElement | null) => {\n        wrapRef.current = node;\n        measureRef(node);\n      },\n      [measureRef],\n    );\n    const width = fluid ? (measuredWidth ?? 600) : widthProp;\n    const showValues = showValuesProp ?? (zero && type === 'bar');\n    const stackedBars = stacked && type === 'bar';\n    const totals = categories.map((_, i) => series.reduce((s, x) => s + (x.values[i] ?? 0), 0));\n    const rawMax = stackedBars ? Math.max(0, ...totals) : Math.max(0, ...series.flatMap((s) => s.values));\n    const max = niceMax(rawMax);\n    const ticks = [0, 1, 2, 3, 4].map((t) => (max / 4) * t);\n    // The y-axis gutter grows with the widest tick label (8px gap to the plot, 8px to the edge), never below the 40px default.\n    const widestTick = axisLabels ? Math.max(...ticks.map((t) => format(t).length)) * AXIS_CHAR_WIDTH : 0;\n    const gutter = Math.max(40, Math.ceil(widestTick) + 16);\n    const left = axisLabels ? (yTitle ? gutter + 16 : gutter) : 8;\n    // Room above the tallest bar for the value labels.\n    const top = showValues && type === 'bar' ? 20 : 8;\n    const right = 8;\n    const plotW = width - left - right;\n    const band = plotW / Math.max(1, categories.length);\n    const labelLines = axisLabels\n      ? categories.map((c) => (wrapLabels ? wrapAxisLabel(c, Math.floor(((labelMaxWidth ?? band) - 4) / AXIS_CHAR_WIDTH), Math.max(1, labelMaxLines)) : [c]))\n      : [];\n    const extraLabelRows = Math.max(0, ...labelLines.map((lines) => lines.length - 1));\n    const bottom = (axisLabels ? (xTitle ? 44 : 24) : 8) + extraLabelRows * AXIS_LINE_HEIGHT;\n    const plotH = height - top - bottom;\n    const y = (v: number) => top + plotH - (v / max) * plotH;\n    const x = (i: number) => left + band * i + band / 2;\n    const barGroupW = band * 0.6;\n\n    const toneOf = (s: ChartSeries, i: number): ChartTone => s.tone ?? toneAt(i);\n    const [active, setActive] = useState<number | null>(null);\n    const [scale, setScale] = useState(1);\n    useEffect(() => {\n      if (active === null) return undefined;\n      const onKey = (event: KeyboardEvent) => {\n        if (event.key === 'Escape') setActive(null);\n      };\n      document.addEventListener('keydown', onKey);\n      return () => document.removeEventListener('keydown', onKey);\n    }, [active]);\n    const show = (i: number) => {\n      const box = wrapRef.current?.clientWidth;\n      setScale(box ? Math.min(1, box / width) : 1);\n      setActive(i);\n    };\n    const derivedList = derived ? (Array.isArray(derived) ? derived : [derived]) : [];\n    const datumAt = (i: number): ChartDatum => ({\n      index: i,\n      category: categories[i],\n      values: Object.fromEntries(series.map((s) => [s.id, s.values[i] ?? 0])),\n      total: totals[i],\n    });\n    const derivedAt = (i: number) =>\n      derivedList.map((d) => {\n        const v = d.compute(datumAt(i));\n        return { id: `derived-${d.key}`, label: d.label, value: v == null || Number.isNaN(v) ? '\\u2013' : (d.format ?? format)(v) };\n      });\n    const table: ChartTable = {\n      columns: [categoryLabel ?? l.chartCategory, ...series.map((s) => s.label), ...derivedList.map((d) => d.label)],\n      rows: categories.map((c, i) => [c, ...series.map((s) => format(s.values[i] ?? 0)), ...derivedAt(i).map((d) => d.value)]),\n    };\n\n    return (\n      <ChartFrame\n        ref={ref}\n        table={table}\n        legend={legend ? series.map((s, i) => ({ id: s.id, label: s.label, tone: toneOf(s, i) })) : undefined}\n        legendPosition={legendPosition}\n        {...frame}\n      >\n        <div ref={setWrap} data-chart-fluid={fluid || undefined} className={cn(tooltip && 'relative max-w-full overflow-hidden [&>svg]:block', fluid && 'w-full min-w-0 max-w-full overflow-hidden [&>svg]:block')} onPointerLeave={tooltip ? () => setActive(null) : undefined}>\n        <svg width={width} height={height} viewBox={`0 0 ${width} ${height}`} aria-hidden=\"true\" focusable=\"false\" className=\"h-auto max-w-full\">\n          <g className=\"text-border-subtle\">\n            {ticks.map((t) => (\n              <line key={t} x1={left} x2={width - right} y1={y(t)} y2={y(t)} className=\"stroke-current\" strokeWidth={1} />\n            ))}\n          </g>\n          {axisLabels ? (\n            <g className={axisText}>\n              {ticks.map((t) => (\n                <text key={t} x={left - 8} y={y(t)} textAnchor=\"end\" dominantBaseline=\"middle\">\n                  {format(t)}\n                </text>\n              ))}\n              {categories.map((c, i) => (\n                <text key={c + i} x={x(i)} y={height - bottom + 16} textAnchor=\"middle\" data-axis-label=\"\">\n                  {labelLines[i].map((line, li) => (\n                    <tspan key={li} x={x(i)} dy={li === 0 ? 0 : AXIS_LINE_HEIGHT}>\n                      {line}\n                    </tspan>\n                  ))}\n                </text>\n              ))}\n              {xTitle ? (\n                <text x={left + plotW / 2} y={height - 6} textAnchor=\"middle\">\n                  {xTitle}\n                </text>\n              ) : null}\n              {yTitle ? (\n                <text transform={`translate(12 ${top + plotH / 2}) rotate(-90)`} textAnchor=\"middle\">\n                  {yTitle}\n                </text>\n              ) : null}\n            </g>\n          ) : null}\n          {type === 'bar'\n            ? categories.map((c, i) => {\n                let acc = 0;\n                return (\n                  <g key={c + i}>\n                    {series.map((s, si) => {\n                      const v = s.values[i] ?? 0;\n                      const w = stackedBars ? barGroupW : barGroupW / series.length - 2;\n                      const bx = stackedBars ? x(i) - w / 2 : x(i) - barGroupW / 2 + si * (barGroupW / series.length) + 1;\n                      const by = stackedBars ? y(acc + v) : y(v);\n                      const bh = stackedBars ? y(acc) - y(acc + v) : y(0) - y(v);\n                      if (stackedBars) acc += v;\n                      const pointColor = s.pointColors?.[i];\n                      const pointTone = s.pointTones?.[i];\n                      const lastSegment = !stackedBars || series.findLastIndex((x2) => (x2.values[i] ?? 0) > 0) === si;\n                      const barProps = {\n                        'data-bar': '',\n                        className: cn('fill-current', toneText[pointTone ?? toneOf(s, si)]),\n                        style: pointColor ? { fill: pointColor } : undefined,\n                      };\n                      return (\n                        <g key={s.id}>\n                          {zero ? (\n                            <path d={topRoundedPath(bx, by, Math.max(0, w), Math.max(0, bh), lastSegment ? BAR_TOP_RADIUS : 0)} {...barProps} />\n                          ) : (\n                            <rect x={bx} y={by} width={Math.max(0, w)} height={Math.max(0, bh)} rx={2} {...barProps} />\n                          )}\n                          {showValues && !stackedBars ? (\n                            <text data-bar-value=\"\" x={bx + Math.max(0, w) / 2} y={by - 4} textAnchor=\"middle\" className={valueText}>\n                              {format(v)}\n                            </text>\n                          ) : null}\n                        </g>\n                      );\n                    })}\n                    {showValues && stackedBars ? (\n                      <text data-bar-value=\"\" x={x(i)} y={y(totals[i]) - 4} textAnchor=\"middle\" className={valueText}>\n                        {format(totals[i])}\n                      </text>\n                    ) : null}\n                  </g>\n                );\n              })\n            : series.map((s, si) => (\n                <g key={s.id} className={toneText[toneOf(s, si)]}>\n                  <polyline\n                    fill=\"none\"\n                    className={toneStroke[toneOf(s, si)]}\n                    strokeWidth={2}\n                    strokeLinejoin=\"round\"\n                    strokeLinecap=\"round\"\n                    points={categories.map((_, i) => `${x(i)},${y(s.values[i] ?? 0)}`).join(' ')}\n                  />\n                  {categories.map((_, i) => (\n                    <circle key={i} cx={x(i)} cy={y(s.values[i] ?? 0)} r={3} className=\"fill-current\" />\n                  ))}\n                </g>\n              ))}\n          {tooltip\n            ? categories.map((c, i) => (\n                <rect\n                  key={c + i}\n                  data-chart-hit=\"\"\n                  x={left + band * i}\n                  y={top}\n                  width={band}\n                  height={plotH}\n                  fill=\"transparent\"\n                  onPointerEnter={() => show(i)}\n                  onPointerMove={() => active !== i && show(i)}\n                />\n              ))\n            : null}\n        </svg>\n        {tooltip && active !== null ? (\n          <ChartTooltip\n            title={categories[active]}\n            anchor={x(active)}\n            scale={scale}\n            containerRef={wrapRef}\n            rows={[...series.map((s, si) => ({ id: s.id, label: s.label, value: format(s.values[active] ?? 0), tone: toneOf(s, si) as ChartTone | undefined })), ...derivedAt(active)]}\n          />\n        ) : null}\n        </div>\n      </ChartFrame>\n    );\n  },\n);\nLineBarChart.displayName = 'LineBarChart';\n\n// Pie chart\n\nexport interface PieSlice {\n  id: string;\n  label: string;\n  value: number;\n  tone?: ChartTone;\n  // Custom colour (stage or project data): a CSS colour such as `var(--colors-data-stage-1)` or a `bg-*` token class. Wins over `tone`.\n  color?: string;\n}\n\nexport interface PieChartProps extends FrameShared {\n  slices: PieSlice[];\n  // 0 renders a pie, 0.5 a donut (Figma default). Default 0.5.\n  hole?: number;\n  centerLabel?: ReactNode;\n  legend?: boolean;\n  // Figma sizes xxs 120, xs 160, sm 200, md 240, lg 280 (px), or a pixel number. Default sm.\n  size?: ChartSize | number;\n}\n\nexport type ChartSize = 'xxs' | 'xs' | 'sm' | 'md' | 'lg';\n\nconst chartSizes: Record<ChartSize, number> = { xxs: 120, xs: 160, sm: 200, md: 240, lg: 280 };\n\n// Fill of an svg shape from a custom colour: a `bg-*` token class becomes its `text-*` twin (fill-current), anything else a CSS colour.\nconst svgColor = (color: string | undefined) =>\n  color ? (color.startsWith('bg-') ? { className: `text-${color.slice(3)}` } : { style: { color } }) : undefined;\n\n// Background of an html shape from a custom colour.\nconst bgColor = (color: string | undefined) =>\n  color ? (color.startsWith('bg-') ? { className: color } : { style: { backgroundColor: color } }) : undefined;\n\nconst polar = (cx: number, cy: number, r: number, angle: number) => [cx + r * Math.cos(angle - Math.PI / 2), cy + r * Math.sin(angle - Math.PI / 2)];\n\nconst PieChart = forwardRef<HTMLElement, PieChartProps>(\n  ({ slices, hole = 0.5, centerLabel, legend = true, legendPosition = 'right', size: sizeProp = 'sm', ...frame }, ref) => {\n    const l = useLabels();\n    const format = useNumberFormat();\n    const size = typeof sizeProp === 'number' ? sizeProp : chartSizes[sizeProp];\n    const total = slices.reduce((s, x) => s + Math.max(0, x.value), 0);\n    const c = size / 2;\n    const r = c - 2;\n    const ri = r * Math.min(0.9, Math.max(0, hole));\n    let start = 0;\n    const paths = slices.map((s, i) => {\n      const frac = total > 0 ? Math.max(0, s.value) / total : 0;\n      const a0 = start;\n      const a1 = start + frac * Math.PI * 2;\n      start = a1;\n      const tone = s.tone ?? toneAt(i);\n      if (frac >= 0.9999) {\n        return (\n          <g key={s.id} className={cn(toneText[tone], svgColor(s.color)?.className)} style={svgColor(s.color)?.style}>\n            <circle cx={c} cy={c} r={r} className=\"fill-current\" />\n            {ri > 0 ? <circle cx={c} cy={c} r={ri} className=\"fill-background\" /> : null}\n          </g>\n        );\n      }\n      const large = a1 - a0 > Math.PI ? 1 : 0;\n      const [x0, y0] = polar(c, c, r, a0);\n      const [x1, y1] = polar(c, c, r, a1);\n      const [xi1, yi1] = polar(c, c, ri, a1);\n      const [xi0, yi0] = polar(c, c, ri, a0);\n      const d =\n        ri > 0\n          ? `M ${x0} ${y0} A ${r} ${r} 0 ${large} 1 ${x1} ${y1} L ${xi1} ${yi1} A ${ri} ${ri} 0 ${large} 0 ${xi0} ${yi0} Z`\n          : `M ${c} ${c} L ${x0} ${y0} A ${r} ${r} 0 ${large} 1 ${x1} ${y1} Z`;\n      return <path key={s.id} d={d} className={cn('fill-current', toneText[tone], svgColor(s.color)?.className)} style={svgColor(s.color)?.style} />;\n    });\n    const table: ChartTable = {\n      columns: [l.chartSlice, l.chartValue, l.chartShare],\n      rows: slices.map((s) => [s.label, format(s.value), total > 0 ? `${format((s.value / total) * 100)} %` : '0 %']),\n    };\n    return (\n      <ChartFrame\n        ref={ref}\n        table={table}\n        legend={legend ? slices.map((s, i) => ({ id: s.id, label: `${s.label} ${total > 0 ? Math.round((s.value / total) * 100) : 0} %`, tone: s.tone ?? toneAt(i), color: s.color })) : undefined}\n        legendPosition={legendPosition}\n        {...frame}\n      >\n        <div className=\"relative inline-block\">\n          <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} aria-hidden=\"true\" focusable=\"false\" className=\"h-auto max-w-full\">\n            {paths}\n          </svg>\n          {centerLabel ? (\n            <span className=\"typography-ui-lg-semibold absolute inset-0 flex items-center justify-center text-foreground\">{centerLabel}</span>\n          ) : null}\n        </div>\n      </ChartFrame>\n    );\n  },\n);\nPieChart.displayName = 'PieChart';\n\n// Radar chart\n\nexport interface RadarChartProps extends FrameShared {\n  axes: string[];\n  series: ChartSeries[];\n  max?: number;\n  rings?: number;\n  legend?: boolean;\n  size?: number;\n}\n\nconst RadarChart = forwardRef<HTMLElement, RadarChartProps>(\n  ({ axes, series, max, rings = 5, legend = true, legendPosition = 'bottom', size = 372, ...frame }, ref) => {\n    const l = useLabels();\n    const format = useNumberFormat();\n    const c = size / 2;\n    const pad = 48;\n    const r = c - pad;\n    const top = niceMax(max ?? Math.max(0, ...series.flatMap((s) => s.values)));\n    const n = Math.max(3, axes.length);\n    const angle = (i: number) => (i / n) * Math.PI * 2;\n    const pt = (i: number, v: number) => polar(c, c, (v / top) * r, angle(i));\n    const ringPoints = (k: number) => axes.map((_, i) => pt(i, (top / rings) * k).join(',')).join(' ');\n    const table: ChartTable = {\n      columns: [l.chartAxis, ...series.map((s) => s.label)],\n      rows: axes.map((a, i) => [a, ...series.map((s) => format(s.values[i] ?? 0))]),\n    };\n    return (\n      <ChartFrame\n        ref={ref}\n        table={table}\n        legend={legend ? series.map((s, i) => ({ id: s.id, label: s.label, tone: s.tone ?? toneAt(i) })) : undefined}\n        legendPosition={legendPosition}\n        {...frame}\n      >\n        <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} aria-hidden=\"true\" focusable=\"false\" className=\"h-auto max-w-full\">\n          <g className=\"text-border-subtle\">\n            {Array.from({ length: rings }, (_, k) => (\n              <polygon key={k} points={ringPoints(k + 1)} fill=\"none\" className=\"stroke-current\" strokeWidth={1} />\n            ))}\n            {axes.map((a, i) => {\n              const [x, y] = pt(i, top);\n              return <line key={a} x1={c} y1={c} x2={x} y2={y} className=\"stroke-current\" strokeWidth={1} />;\n            })}\n          </g>\n          {series.map((s, si) => (\n            <g key={s.id} className={toneText[s.tone ?? toneAt(si)]}>\n              <polygon\n                points={axes.map((_, i) => pt(i, s.values[i] ?? 0).join(',')).join(' ')}\n                fillOpacity={0.2}\n                className={cn('fill-current', toneStroke[s.tone ?? toneAt(si)])}\n                strokeWidth={2}\n                strokeLinejoin=\"round\"\n              />\n            </g>\n          ))}\n          <g className=\"typography-text-extra-small fill-current text-muted-foreground\">\n            {axes.map((a, i) => {\n              const [x, y] = polar(c, c, r + 16, angle(i));\n              return (\n                <text key={a} x={x} y={y} textAnchor=\"middle\" dominantBaseline=\"middle\">\n                  {a}\n                </text>\n              );\n            })}\n          </g>\n        </svg>\n      </ChartFrame>\n    );\n  },\n);\nRadarChart.displayName = 'RadarChart';\n\n// Activity gauge\n\nexport interface ActivityGaugeSeries {\n  id: string;\n  label: string;\n  value: number;\n  tone?: ChartTone;\n}\n\nexport interface ActivityGaugeProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {\n  'aria-label': string;\n  // One value as shorthand for a single ring. Ignored when `series` is set.\n  value?: number;\n  // Up to three concentric rings, outermost first.\n  series?: ActivityGaugeSeries[];\n  max?: number;\n  tone?: ChartTone;\n  // Caption above the center value, e.g. \"Active users\".\n  label?: ReactNode;\n  // Center text. Defaults to the first ring's value.\n  valueText?: ReactNode;\n  // Figma sizes xs 160, sm 200, md 240, lg 280 (px), or a pixel number. Default xs.\n  size?: Exclude<ChartSize, 'xxs'> | number;\n  legend?: 'right' | 'bottom' | false;\n}\n\nconst ActivityGauge = forwardRef<HTMLDivElement, ActivityGaugeProps>(\n  ({ className, 'aria-label': ariaLabel, value = 0, series, max = 100, tone = 'brand', label, valueText, size: sizeProp = 'xs', legend = false, ...props }, ref) => {\n    const l = useLabels();\n    const format = useNumberFormat();\n    const size = typeof sizeProp === 'number' ? sizeProp : chartSizes[sizeProp];\n    const rings: ActivityGaugeSeries[] = series ?? [{ id: 'value', label: ariaLabel, value, tone }];\n    const stroke = size / 16;\n    const gap = size / 60;\n    const center = size / 2;\n    const clampOf = (v: number) => Math.min(Math.max(v, 0), max);\n    const single = rings.length === 1;\n    const lead = clampOf(rings[0]?.value ?? 0);\n    const big = size >= 240;\n    const meterProps = single\n      ? { role: 'meter', 'aria-valuemin': 0, 'aria-valuemax': max, 'aria-valuenow': lead, 'aria-valuetext': l.valueOfMax(format(lead), format(max)) }\n      : { role: 'group' };\n    return (\n      <div className={cn('inline-flex gap-4', legend === 'bottom' ? 'flex-col items-center' : 'flex-row items-center', className)} ref={ref} {...props}>\n        <div className=\"relative shrink-0\" style={{ width: size, height: size }} aria-label={ariaLabel} {...meterProps}>\n          <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`} aria-hidden=\"true\" focusable=\"false\" className=\"-rotate-90\">\n            {rings.slice(0, 3).map((ring, i) => {\n              const r = center - stroke / 2 - i * (stroke + gap);\n              const pct = max > 0 ? (clampOf(ring.value) / max) * 100 : 0;\n              return (\n                <g key={ring.id}>\n                  <circle cx={center} cy={center} r={r} fill=\"none\" strokeWidth={stroke} className=\"stroke-current text-muted\" />\n                  {pct > 0 ? (\n                    <circle cx={center} cy={center} r={r} fill=\"none\" strokeWidth={stroke} strokeLinecap=\"round\" pathLength={100} strokeDasharray={`${pct} 100`} className={toneStroke[ring.tone ?? (single ? tone : toneAt(i))]} />\n                  ) : null}\n                </g>\n              );\n            })}\n          </svg>\n          <div className=\"absolute inset-0 flex flex-col items-center justify-center\">\n            {label ? <span className=\"typography-text-extra-small text-muted-foreground\">{label}</span> : null}\n            <span className={cn('tabular-nums text-foreground', big ? 'typography-kpi-value' : 'typography-ui-lg-semibold')}>{valueText ?? format(lead)}</span>\n          </div>\n          {single ? null : (\n            <div className=\"sr-only\">\n              {rings.map((ring) => (\n                <div key={ring.id} role=\"meter\" aria-label={ring.label} aria-valuemin={0} aria-valuemax={max} aria-valuenow={clampOf(ring.value)} aria-valuetext={l.valueOfMax(format(clampOf(ring.value)), format(max))} />\n              ))}\n            </div>\n          )}\n        </div>\n        {legend ? <ChartLegend layout={legend === 'right' ? 'column' : 'row'} items={rings.map((r, i) => ({ id: r.id, label: r.label, tone: r.tone ?? (single ? tone : toneAt(i)) }))} /> : null}\n      </div>\n    );\n  },\n);\nActivityGauge.displayName = 'ActivityGauge';\n\n// Sparkline\n\nexport interface SparklineProps extends Omit<HTMLAttributes<HTMLSpanElement>, 'children'> {\n  'aria-label': string;\n  values: number[];\n  tone?: ChartTone;\n  width?: number;\n  height?: number;\n}\n\nconst Sparkline = forwardRef<HTMLSpanElement, SparklineProps>(({ className, values, tone = 'brand', width = 56, height = 18, ...props }, ref) => {\n  const min = Math.min(...values);\n  const maxV = Math.max(...values);\n  const span = maxV - min || 1;\n  const pad = 2;\n  const pts = values.map((v, i) => [pad + (i / Math.max(1, values.length - 1)) * (width - pad * 2), pad + (1 - (v - min) / span) * (height - pad * 2)]);\n  const last = pts[pts.length - 1];\n  return (\n    <span ref={ref} role=\"img\" className={cn('inline-flex', toneText[tone], className)} {...props}>\n      <svg width={width} height={height} viewBox={`0 0 ${width} ${height}`} aria-hidden=\"true\" focusable=\"false\">\n        {pts.length > 1 ? <polyline fill=\"none\" className={toneStroke[tone]} strokeWidth={1.5} strokeLinecap=\"round\" strokeLinejoin=\"round\" points={pts.map((p) => p.join(',')).join(' ')} /> : null}\n        {last ? <circle cx={last[0]} cy={last[1]} r={2} className=\"fill-current\" /> : null}\n      </svg>\n    </span>\n  );\n});\nSparkline.displayName = 'Sparkline';\n\n// Histogram\n\nexport interface HistogramBin {\n  label?: string;\n  value: number;\n}\n\nexport interface HistogramProps extends FrameShared {\n  bins: HistogramBin[];\n  // Inclusive bin index range drawn in the brand tone, the rest is neutral.\n  highlight?: [number, number];\n  // Axis labels under every n-th bin that has a label.\n  axisLabels?: boolean;\n  width?: number;\n  height?: number;\n}\n\nconst Histogram = forwardRef<HTMLElement, HistogramProps>(({ bins, highlight, axisLabels = true, width = 320, height = 144, ...frame }, ref) => {\n  const l = useLabels();\n  const format = useNumberFormat();\n  const bottom = axisLabels ? 20 : 0;\n  const plotH = height - bottom;\n  const maxV = Math.max(1, ...bins.map((b) => b.value));\n  const gap = 4;\n  const bw = (width - gap * (bins.length - 1)) / Math.max(1, bins.length);\n  const table: ChartTable = { columns: [l.chartBin, l.chartCount], rows: bins.map((b, i) => [b.label ?? String(i + 1), format(b.value)]) };\n  return (\n    <ChartFrame ref={ref} table={table} {...frame}>\n      <svg width={width} height={height} viewBox={`0 0 ${width} ${height}`} aria-hidden=\"true\" focusable=\"false\" className=\"h-auto max-w-full\">\n        {bins.map((b, i) => {\n          const on = highlight ? i >= highlight[0] && i <= highlight[1] : false;\n          const h = (b.value / maxV) * plotH;\n          return <rect key={i} x={i * (bw + gap)} y={plotH - h} width={bw} height={h} rx={2} className={cn('fill-current', on ? toneText.brand : toneText.neutral)} />;\n        })}\n        {axisLabels ? (\n          <g className=\"typography-text-extra-small fill-current text-muted-foreground\">\n            {bins.map((b, i) =>\n              b.label ? (\n                <text key={i} x={i * (bw + gap) + bw / 2} y={height - 4} textAnchor=\"middle\">\n                  {b.label}\n                </text>\n              ) : null,\n            )}\n          </g>\n        ) : null}\n      </svg>\n    </ChartFrame>\n  );\n});\nHistogram.displayName = 'Histogram';\n\n// Horizontal bar list\n\nexport interface HBarItem {\n  id: string;\n  label: string;\n  value: number;\n  // Text shown at the right, e.g. \"3 proj.\". Defaults to the value.\n  valueLabel?: ReactNode;\n  tone?: ChartTone;\n  // Custom colour (stage or project data): a CSS colour or a `bg-*` token class. Wins over `tone`. Ignored when the row has `segments`.\n  color?: string;\n  // Splits the bar into stacked parts (for example open, in progress, done). The row total is the sum of the parts and replaces `value`. A segment id shares its tone and legend entry across rows.\n  segments?: HBarSegment[];\n}\n\nexport interface HBarSegment {\n  id: string;\n  label: string;\n  value: number;\n  tone?: ChartTone;\n  // Custom colour: a CSS colour or a `bg-*` token class. Wins over `tone`.\n  color?: string;\n}\n\nexport interface HBarListProps extends Omit<HTMLAttributes<HTMLUListElement>, 'children'> {\n  'aria-label': string;\n  items: HBarItem[];\n  // Upper bound for the bars. Defaults to the largest value.\n  max?: number;\n  // Stacked puts label and value above the bar instead of beside it.\n  layout?: 'inline' | 'stacked';\n  // Legend of the segment ids used by `items[].segments`. Default shows it whenever a row has segments.\n  legend?: boolean;\n}\n\nconst rowTotal = (item: HBarItem) => (item.segments ? item.segments.reduce((sum, seg) => sum + Math.max(0, seg.value), 0) : item.value);\n\nconst HBarList = forwardRef<HTMLUListElement, HBarListProps>(({ className, items, max, layout = 'inline', legend, ...props }, ref) => {\n  const format = useNumberFormat();\n  const top = max ?? Math.max(1, ...items.map(rowTotal));\n  const segmentLegend: ChartLegendItem[] = [];\n  items.forEach((item) =>\n    item.segments?.forEach((seg, si) => {\n      if (!segmentLegend.some((entry) => entry.id === seg.id)) segmentLegend.push({ id: seg.id, label: seg.label, tone: seg.tone ?? toneAt(si), color: seg.color });\n    }),\n  );\n  const showLegend = (legend ?? true) && segmentLegend.length > 0;\n  const list = (\n    <ul ref={ref} className={cn('typography-ui m-0 flex list-none flex-col gap-3 p-0 text-foreground', className)} {...props}>\n      {items.map((item) => {\n        const total = rowTotal(item);\n        // `flex-1` sets flex-basis 0 on the main axis. In the stacked column that is the height axis, which overrides `h-2` and collapses the bar to 0px, so the stacked bar takes the full width and keeps its height instead.\n        const barSize = layout === 'inline' ? 'flex-1' : 'w-full shrink-0';\n        const pct = Math.min(100, Math.max(0, (total / top) * 100));\n        const bar = item.segments ? (\n          <>\n            <span aria-hidden=\"true\" data-segmented=\"\" className={cn('flex h-2 overflow-hidden rounded-full bg-muted', barSize)}>\n              {item.segments.map((seg, si) => (\n                <span\n                  key={seg.id}\n                  data-segment={seg.id}\n                  className={cn('block h-full', seg.color ? bgColor(seg.color)?.className : toneBg[seg.tone ?? toneAt(si)])}\n                  style={{ width: `${Math.min(100, Math.max(0, (seg.value / top) * 100))}%`, ...bgColor(seg.color)?.style }}\n                />\n              ))}\n            </span>\n            <span className=\"sr-only\">{item.segments.map((seg) => `${seg.label}: ${format(seg.value)}`).join(', ')}</span>\n          </>\n        ) : (\n          <span aria-hidden=\"true\" className={cn('block h-2 overflow-hidden rounded-full bg-muted', barSize)}>\n            <span\n              className={cn('block h-full rounded-full', item.color ? bgColor(item.color)?.className : toneBg[item.tone ?? 'brand'])}\n              style={{ width: `${pct}%`, ...bgColor(item.color)?.style }}\n            />\n          </span>\n        );\n        return (\n          <li key={item.id} data-layout={layout} className={cn('gap-2', layout === 'inline' ? 'flex items-center' : 'flex flex-col')}>\n            {layout === 'inline' ? (\n              <>\n                <span className=\"w-1/3 min-w-0 shrink-0 truncate\">{item.label}</span>\n                {bar}\n                <span className=\"typography-ui-semibold shrink-0 tabular-nums\">{item.valueLabel ?? format(total)}</span>\n              </>\n            ) : (\n              <>\n                <span className=\"flex items-baseline justify-between gap-2\">\n                  <span className=\"min-w-0 truncate\">{item.label}</span>\n                  <span className=\"typography-ui-semibold shrink-0 tabular-nums\">{item.valueLabel ?? format(total)}</span>\n                </span>\n                {bar}\n              </>\n            )}\n          </li>\n        );\n      })}\n    </ul>\n  );\n  if (!showLegend) return list;\n  return (\n    <div className=\"flex min-w-0 flex-col gap-3\">\n      {list}\n      <ChartLegend items={segmentLegend} />\n    </div>\n  );\n});\nHBarList.displayName = 'HBarList';\n\n// Treemap\n\nexport interface TreemapItem {\n  id: string;\n  label: string;\n  value: number;\n  tone?: ChartTone;\n  // Custom colour (stage or project data): a CSS colour or a `bg-*` token class. Wins over `tone`.\n  color?: string;\n}\n\ninterface Rect {\n  x: number;\n  y: number;\n  w: number;\n  h: number;\n}\n\nconst squarify = (items: { id: string; area: number }[], box: Rect): Record<string, Rect> => {\n  const out: Record<string, Rect> = {};\n  let rest = [...items].sort((a, b) => b.area - a.area);\n  let { x, y, w, h } = box;\n  const worst = (row: number[], side: number) => {\n    const s = row.reduce((a, b) => a + b, 0);\n    if (s === 0) return Infinity;\n    const mx = Math.max(...row);\n    const mn = Math.min(...row);\n    return Math.max((side * side * mx) / (s * s), (s * s) / (side * side * mn));\n  };\n  while (rest.length > 0) {\n    const side = Math.min(w, h);\n    const row: typeof rest = [];\n    let i = 0;\n    while (i < rest.length) {\n      const candidate = [...row, rest[i]];\n      if (row.length === 0 || worst(candidate.map((c) => c.area), side) <= worst(row.map((c) => c.area), side)) {\n        row.push(rest[i]);\n        i += 1;\n      } else break;\n    }\n    const sum = row.reduce((a, b) => a + b.area, 0);\n    if (w >= h) {\n      const rw = sum / h;\n      let cy = y;\n      for (const r of row) {\n        const rh = r.area / rw;\n        out[r.id] = { x, y: cy, w: rw, h: rh };\n        cy += rh;\n      }\n      x += rw;\n      w -= rw;\n    } else {\n      const rh = sum / w;\n      let cx = x;\n      for (const r of row) {\n        const rw = r.area / rh;\n        out[r.id] = { x: cx, y, w: rw, h: rh };\n        cx += rw;\n      }\n      y += rh;\n      h -= rh;\n    }\n    rest = rest.slice(row.length);\n  }\n  return out;\n};\n\nexport interface TreemapProps extends FrameShared {\n  items: TreemapItem[];\n  // Maximum width in px, or 'auto' to fill the container (the cells are percent based, so the layout follows the container at any width). Default 560.\n  width?: number | 'auto';\n  height?: number;\n  // Cells narrower or lower than this share of the box (percent) hide their label.\n  minLabelPercent?: number;\n}\n\nconst Treemap = forwardRef<HTMLElement, TreemapProps>(({ items, width = 560, height = 220, minLabelPercent = 10, ...frame }, ref) => {\n  const l = useLabels();\n  const format = useNumberFormat();\n  const total = items.reduce((s, i) => s + Math.max(0, i.value), 0);\n  const positive = items.filter((i) => i.value > 0);\n  const rects = total > 0 ? squarify(positive.map((i) => ({ id: i.id, area: (i.value / total) * 100 * 100 })), { x: 0, y: 0, w: 100, h: 100 }) : {};\n  const table: ChartTable = { columns: [l.chartItem, l.chartValue], rows: items.map((i) => [i.label, format(i.value)]) };\n  return (\n    <ChartFrame ref={ref} table={table} {...frame}>\n      <div className=\"relative w-full overflow-hidden rounded-control\" style={{ maxWidth: width === 'auto' ? undefined : width, height }}>\n        {positive.map((item, idx) => {\n          const r = rects[item.id];\n          if (!r) return null;\n          const showLabel = r.w >= minLabelPercent && r.h >= minLabelPercent;\n          return (\n            <div\n              key={item.id}\n              title={`${item.label} ${format(item.value)}`}\n              className={cn('typography-ui absolute flex flex-col justify-end border border-solid border-background p-2', item.color ? [bgColor(item.color)?.className, customOn[item.color] ?? 'text-text-on-brand'] : [toneBg[item.tone ?? toneAt(idx)], toneOn[item.tone ?? toneAt(idx)]])}\n              style={{ ...bgColor(item.color)?.style, left: `${r.x}%`, top: `${r.y}%`, width: `${r.w}%`, height: `${r.h}%` }}\n            >\n              {showLabel ? (\n                <>\n                  <span className=\"typography-ui-medium truncate\">{item.label}</span>\n                  <span className=\"typography-text-extra-small tabular-nums\">{format(item.value)}</span>\n                </>\n              ) : null}\n            </div>\n          );\n        })}\n      </div>\n    </ChartFrame>\n  );\n});\nTreemap.displayName = 'Treemap';\n\nexport { ActivityGauge, ChartFrame, ChartLegend, HBarList, Histogram, LineBarChart, PieChart, RadarChart, Sparkline, Treemap };\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}