{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "bar-chart",
  "type": "registry:component",
  "title": "Bar Chart",
  "description": "Categories compared by length, grouped or stacked.",
  "dependencies": [
    "@base-ui/react",
    "@tanstack/charts",
    "tailwind-variants"
  ],
  "registryDependencies": [],
  "cssVars": {
    "light": {
      "success": "oklch(0.696 0.17 162.48)",
      "warning": "oklch(0.769 0.188 70.08)",
      "info": "oklch(0.623 0.214 259.815)"
    },
    "dark": {
      "success": "oklch(0.696 0.17 162.48)",
      "warning": "oklch(0.769 0.188 70.08)",
      "info": "oklch(0.623 0.214 259.815)"
    }
  },
  "files": [
    {
      "path": "components/panelui/bar-chart.tsx",
      "type": "registry:component",
      "target": "@components/panelui/bar-chart.tsx",
      "content": "'use client';\n\n/**\n * BarChart — categories compared by length, grouped or stacked.\n *\n * ```tsx\n * <BarChart data={revenue} xDataKey=\"month\">\n *   <BarChart.Grid />\n *   <BarChart.Bar dataKey=\"revenue\" />\n *   <BarChart.XAxis />\n *   <BarChart.Tooltip />\n * </BarChart>\n * ```\n *\n * ## The same parts as the app\n *\n * The parts and their props are the app library's BarChart, so a chart written\n * for one reads the same in the other. What differs is what the web changes: a\n * colour is named by the theme's role, a band is read by hovering, tapping or\n * dragging, a click pins the reading, and the arrow keys walk the bands.\n *\n * ## The parts describe, the root draws\n *\n * `Grid`, `Bar`, the axes and `Tooltip` draw nothing themselves. Each one tells\n * the root what it wants, and the root builds one chart definition from them.\n * They are found by context and not by inspecting children, so a part can be\n * wrapped in a component of your own. It also means the chart draws in the\n * browser, in a box its `aspectRatio` has reserved.\n *\n * ## The baseline is zero\n *\n * A bar is read by its length, so a bar cropped at the bottom misreports its\n * value: twice as tall stops meaning twice as much. The derived domain always\n * reaches zero. `yDomain` can say otherwise, for a chart that accepts that.\n *\n * ## Rows become one row per bar\n *\n * The data arrives as one row per category with a column per series. The marks\n * want one row per bar, carrying its category's index rather than its name, so\n * two bands can share a label.\n */\nimport {\n  barX,\n  barY,\n  defineChart,\n  group,\n  stack,\n  type ChartInteractionController,\n  type ChartMotionContext,\n  type ChartMark,\n  type ChartPoint,\n  type SceneNode,\n} from '@tanstack/charts';\nimport { motion } from '@tanstack/charts/motion';\nimport { Chart } from '@tanstack/charts/react/core';\nimport { scaleBand } from '@tanstack/charts/scales/band';\nimport { scaleLinear } from '@tanstack/charts/scales/linear';\nimport { tooltip as chartTooltip } from '@tanstack/charts/tooltip';\nimport {\n  createContext,\n  forwardRef,\n  useCallback,\n  useContext,\n  useEffect,\n  useId,\n  useImperativeHandle,\n  useMemo,\n  useRef,\n  useState,\n  type ComponentProps,\n  type PointerEvent as ReactPointerEvent,\n  type ReactNode,\n} from 'react';\nimport { tv } from 'tailwind-variants';\nimport { mergeProps } from '@base-ui/react/merge-props';\nimport { useRender } from '@base-ui/react/use-render';\nimport type { ElementType as RenderElementType } from 'react';\n\n/** Draws a part as another element, or composes it with a component: `render={<a href=\"/\" />}`. */\ntype RenderProp<Tag extends RenderElementType> = { render?: useRender.ComponentProps<Tag>['render'] };\n\nexport type BarChartStatus = 'loading' | 'ready';\nexport type BarChartOrientation = 'vertical' | 'horizontal';\nexport type BarChartDatum = Record<string, string | number | null | undefined>;\n\n/** Which of the theme's roles a series is drawn in. */\nexport type BarChartColor =\n  | 'primary'\n  | 'info'\n  | 'success'\n  | 'warning'\n  | 'destructive'\n  | 'chart-1'\n  | 'chart-2'\n  | 'chart-3'\n  | 'chart-4'\n  | 'chart-5';\n\nconst COLOR_VAR: Record<BarChartColor, string> = {\n  primary: 'var(--primary)',\n  info: 'var(--info)',\n  success: 'var(--success)',\n  warning: 'var(--warning)',\n  destructive: 'var(--destructive)',\n  'chart-1': 'var(--chart-1)',\n  'chart-2': 'var(--chart-2)',\n  'chart-3': 'var(--chart-3)',\n  'chart-4': 'var(--chart-4)',\n  'chart-5': 'var(--chart-5)',\n};\n\nconst THEME = {\n  foreground: 'var(--foreground)',\n  muted: 'var(--muted-foreground)',\n  grid: 'var(--border)',\n  background: 'transparent',\n  palette: ['var(--chart-1)', 'var(--chart-2)', 'var(--chart-3)', 'var(--chart-4)', 'var(--chart-5)'],\n};\n\nconst barChartVariants = tv({\n  slots: {\n    root: 'relative grid w-full grid-cols-1 text-foreground',\n    plot: 'relative col-start-1 row-start-2 w-full',\n    // `pan-y` hands a vertical drag to the page and a sideways one to the chart.\n    chart:\n      'absolute! inset-0 touch-pan-y rounded-lg text-xs [--ts-chart-tooltip-background:var(--popover)] [--ts-chart-tooltip-border-radius:var(--radius-xl)] [--ts-chart-tooltip-border:1px_solid_var(--border)] [--ts-chart-tooltip-color:var(--popover-foreground)] [--ts-chart-tooltip-shadow:var(--shadow-lg)] [&_svg]:rounded-lg [&_svg]:outline-none [&_svg:focus-visible]:ring-2 [&_svg:focus-visible]:ring-ring',\n    sweepClip: 'pointer-events-none absolute inset-0 overflow-hidden',\n    sweep:\n      'absolute inset-y-0 left-0 w-2/5 bg-[linear-gradient(90deg,transparent,color-mix(in_oklab,var(--chart-1)_55%,transparent),transparent)] motion-reduce:hidden',\n    legend:\n      'pointer-events-none absolute right-2 top-1 col-start-1 row-start-2 flex gap-3 text-xs text-muted-foreground',\n    legendItem: 'flex items-center gap-1.5',\n    legendSwatch: 'size-2 shrink-0 rounded-full',\n    header: 'col-start-1 row-start-1 flex items-start justify-between gap-3 pb-3',\n    headerText: 'flex min-w-0 flex-1 flex-col gap-0.5',\n    headerMuted: 'text-xs text-muted-foreground',\n    headerValue: 'text-xl font-bold tabular-nums tracking-tight',\n    headerTrailing: 'flex shrink flex-wrap items-center justify-end gap-x-3 gap-y-1 pt-1 text-xs text-muted-foreground',\n  },\n});\n\n/** The value-axis gutter held while loading, when there are no labels to size it from. */\nconst LOADING_GUTTER = 44;\n\n/** How much of the value axis a placeholder bar takes. */\nconst SKELETON_LENGTH = 0.22;\n\n/** Bands to draw when there is no data yet to count them from. */\nconst SKELETON_BARS = 7;\n\nconst SKELETON_MARK = 'bar-chart-skeleton';\n\n/** How much of the entrance goes to staggering the bands. */\nconst STAGGER_SHARE = 0.45;\n\nconst KEYFRAMES = '@keyframes panelui-bar-sweep{from{transform:translateX(-100%)}to{transform:translateX(250%)}}';\n\nconst COMPACT = new Intl.NumberFormat('en', { notation: 'compact', maximumFractionDigits: 1 });\nconst COMPACT_SMALL = new Intl.NumberFormat('en', { notation: 'compact', maximumFractionDigits: 2 });\n\n/** Below one, a single decimal would round most values down to nothing. */\nconst compactNumber = (value: number) => (Math.abs(value) < 1 ? COMPACT_SMALL : COMPACT).format(value);\n\n/** One bar: a value of one series in one band. */\ninterface BarRow {\n  index: number;\n  series: string;\n  value: number;\n}\n\ninterface SeriesEntry {\n  key: string;\n  fill: string;\n  radius: number | undefined;\n  texture: BarChartTexture;\n  dotSize: number;\n  dotGap: number;\n}\n\n/** How a bar is filled: solid colour, or a grid of dots. */\nexport type BarChartTexture = 'solid' | 'dots';\n\n/**\n * What the parts have asked for. An axis formatter is part of the drawing, so a new one redraws it.\n * The readout's are read through `latest` when it opens, so a new one costs nothing.\n */\ninterface Slots {\n  grid?: { rows: number; dashArray: string; opacity: number };\n  xAxis?: Pick<BarChartXAxisProps, 'ticks' | 'format'>;\n  yAxis?: { ticks: number; format: BarChartYAxisProps['format'] };\n  tooltip?: { className: string | undefined; latest: { current: Pick<BarChartTooltipProps, 'formatValue' | 'formatX' | 'labels'> } };\n  skeleton?: { bars: number | undefined; duration: number };\n}\n\ninterface BarChartContextValue {\n  data: readonly BarChartDatum[];\n  xDataKey: string;\n  series: readonly SeriesEntry[];\n  setSeries: (entry: SeriesEntry) => void;\n  removeSeries: (key: string) => void;\n  setSlot: <K extends keyof Slots>(slot: K, value: Slots[K]) => void;\n  activeIndex: number;\n}\n\nconst BarChartContext = createContext<BarChartContextValue | null>(null);\n\nfunction useChart(component: string): BarChartContextValue {\n  const context = useContext(BarChartContext);\n  if (!context) {\n    throw new Error(`${component} must be used within a <BarChart>`);\n  }\n  return context;\n}\n\n/**\n * The band being read, for something rendered inside the chart. A readout\n * outside it takes `onActiveIndexChange` instead.\n */\nexport function useBarChart() {\n  const { data, activeIndex, xDataKey } = useChart('useBarChart');\n  return {\n    activeIndex,\n    activePoint: activeIndex >= 0 ? (data[activeIndex] ?? null) : null,\n    xDataKey,\n  };\n}\n\ntype BarMark = ChartMark<BarRow, number, number, number, number>;\n\n/**\n * Applies the two lengths the marks have no option for, in pixels, once the\n * scales have placed each bar: the gap at the tip of a stacked segment, and the\n * floor under a value too small to see.\n */\nfunction withBarLengths<TMark extends BarMark>(\n  mark: TMark,\n  { gap, floor, horizontal }: { gap: number; floor: number; horizontal: boolean }\n): TMark {\n  const adjust = (node: SceneNode): SceneNode => {\n    if (node.kind === 'group') return { ...node, children: node.children.map(adjust) };\n    if (node.kind !== 'rect' || !node.interaction?.point) return node;\n    const { value } = node.interaction.point.datum as BarRow;\n    if (value === 0) return node;\n    const length = horizontal ? node.width : node.height;\n    const next = Math.max(length - gap, floor);\n    // The tip is what moves: up or right for a positive value, the other way for a negative one.\n    const tipAtStart = horizontal ? value < 0 : value > 0;\n    return horizontal\n      ? { ...node, width: next, x: tipAtStart ? node.x + length - next : node.x }\n      : { ...node, height: next, y: tipAtStart ? node.y + length - next : node.y };\n  };\n\n  return {\n    ...mark,\n    initialize(context) {\n      const initialized = mark.initialize(context);\n      return {\n        ...initialized,\n        postDomain(scene) {\n          const base = initialized.postDomain ? initialized.postDomain(scene) : scene;\n          return { ...base, nodes: base.nodes.map(adjust) };\n        },\n      };\n    },\n  };\n}\n\n/** The stubs' outlines from the drawn scene, for the sweep to be clipped to. */\nfunction stubOutlines(nodes: readonly SceneNode[], x = 0, y = 0, inside = false): string {\n  return nodes\n    .map((node) => {\n      if (node.kind === 'group') {\n        return stubOutlines(\n          node.children,\n          x + (node.translateX ?? 0),\n          y + (node.translateY ?? 0),\n          inside || node.key.includes(SKELETON_MARK)\n        );\n      }\n      if (node.kind !== 'rect' || !(inside || node.key.includes(SKELETON_MARK))) return '';\n      const left = node.x + x;\n      const top = node.y + y;\n      return `M${left},${top}h${node.width}v${node.height}h${-node.width}Z`;\n    })\n    .join('');\n}\n\nexport interface BarChartProps extends Omit<ComponentProps<'div'>, 'ref'> {\n  /** The rows. Each one is a band along the category axis. */\n  data: readonly BarChartDatum[];\n  /** Key holding the category label. Used by the axis and the readout. */\n  xDataKey?: string;\n  /**\n   * `loading` holds the bars back and grows them in when it turns `ready`. Add\n   * a `BarChart.Skeleton` for something to stand in the plot meanwhile.\n   */\n  status?: BarChartStatus;\n  /** The plot's width ÷ height. `2` is the wide card shape. */\n  aspectRatio?: number;\n  /** Milliseconds for the bars to grow in. */\n  animationDuration?: number;\n  /** Milliseconds for the bars and the axis to settle after the data changes. */\n  domainDuration?: number;\n  /**\n   * Fix the value axis instead of deriving it. The derived domain always\n   * includes zero, and bar lengths cannot be compared on a domain that does\n   * not.\n   */\n  yDomain?: [number, number];\n  /** `vertical` grows the bars upward; `horizontal` grows them rightward. */\n  orientation?: BarChartOrientation;\n  /** Stack the series on each other instead of standing them side by side. */\n  stacked?: boolean;\n  /** Fraction of each band left empty, `0` to `1`. */\n  barGap?: number;\n  /** Largest bar thickness in pixels. Derived from the band when omitted. */\n  barWidth?: number;\n  /** Pixels between the segments of a stack. */\n  stackGap?: number;\n  /** Corner radius on the growing end of a bar. */\n  cornerRadius?: number;\n  /** Smallest length a non-zero bar is drawn at, in pixels. */\n  minBarLength?: number;\n  /** Opacity of the bars that are not being read. */\n  fadedOpacity?: number;\n  /**\n   * The band being read, and `-1`/`null` when there is none. Fires when the\n   * index changes, not per frame.\n   */\n  onActiveIndexChange?: (index: number, datum: BarChartDatum | null) => void;\n  /** Drop the axis padding, for a bar sparkline with no axis or readout. */\n  compact?: boolean;\n  /** What a screen reader calls the chart. */\n  label?: string;\n  /** What a screen reader says for one row. Defaults to its keys and values. */\n  labelForDatum?: (datum: BarChartDatum, index: number) => string;\n}\n\n/** Imperative handle: re-run the grow-in, for a \"replay\" control. */\nexport interface BarChartHandle {\n  replay: () => void;\n}\n\nconst BarChartRoot = forwardRef<BarChartHandle, BarChartProps & RenderProp<'div'>>(function BarChartRoot(\n  {\n    data,\n    xDataKey = 'name',\n    status = 'ready',\n    aspectRatio = 2,\n    animationDuration = 700,\n    domainDuration = 500,\n    yDomain,\n    orientation = 'vertical',\n    stacked = false,\n    barGap = 0.2,\n    barWidth,\n    stackGap = 0,\n    cornerRadius = 4,\n    minBarLength = 0,\n    fadedOpacity = 0.3,\n    onActiveIndexChange,\n    compact = false,\n    label,\n    labelForDatum,\n    className,\n    children,\n    render,\n    ...props\n  },\n  ref\n) {\n  const slotClasses = barChartVariants();\n  const plotRef = useRef<HTMLDivElement>(null);\n  const [width, setWidth] = useState(0);\n  // Pattern ids for dotted bars, unique to this chart so two on a page do not share one.\n  const texturePrefix = `panelui-bar-dots-${useId().replace(/[^a-zA-Z0-9_-]/g, '')}`;\n  const [series, setSeriesState] = useState<readonly SeriesEntry[]>([]);\n  const [slots, setSlots] = useState<Slots>({});\n  const [activeIndex, setActiveIndex] = useState(-1);\n  const [revision, setRevision] = useState(0);\n  const [stubs, setStubs] = useState('');\n  const interactionRef = useRef<ChartInteractionController<BarRow, number, number> | null>(null)\n  const dragging = useRef(false);\n\n  /*\n   * The chart mounts once its width is known. Mounted at a guess, the first\n   * measurement is a resize, and a resize ends the entrance before it is seen.\n   */\n  useEffect(() => {\n    const node = plotRef.current;\n    if (!node) return;\n    const read = () => setWidth(node.clientWidth);\n    read();\n    const observer = new ResizeObserver(read);\n    observer.observe(node);\n    return () => observer.disconnect();\n  }, []);\n\n  const setSeries = useCallback(\n    (entry: SeriesEntry) =>\n      setSeriesState((current) =>\n        current.some(({ key }) => key === entry.key)\n          ? current.map((existing) => (existing.key === entry.key ? entry : existing))\n          : [...current, entry]\n      ),\n    []\n  );\n  const removeSeries = useCallback(\n    (key: string) => setSeriesState((current) => current.filter((entry) => entry.key !== key)),\n    []\n  );\n  const setSlot = useCallback<BarChartContextValue['setSlot']>(\n    (slot, value) => setSlots((current) => ({ ...current, [slot]: value })),\n    []\n  );\n\n  useImperativeHandle(ref, () => ({ replay: () => setRevision((current) => current + 1) }), []);\n\n  const loading = status === 'loading';\n  const horizontal = orientation === 'horizontal';\n  const { grid, xAxis, yAxis, tooltip, skeleton } = slots;\n\n  const definition = useMemo(() => {\n    const keys = series.map(({ key }) => key);\n    const bySeries = new Map(series.map((entry) => [entry.key, entry]));\n    const rows: BarRow[] = data.flatMap((datum, index) =>\n      series.flatMap(({ key }) => {\n        const value = datum[key];\n        return typeof value === 'number' && !Number.isNaN(value) ? [{ index, series: key, value }] : [];\n      })\n    );\n\n    const showStubs = loading && skeleton !== undefined;\n    const bands = loading ? Math.max(1, skeleton?.bars ?? (data.length || SKELETON_BARS)) : data.length;\n    const indexes = Array.from({ length: bands }, (_unused, index) => index);\n    const stubRows: BarRow[] = showStubs ? indexes.map((index) => ({ index, series: '', value: SKELETON_LENGTH })) : [];\n\n    const shared = {\n      key: (row: BarRow) => `${row.index}:${row.series}`,\n      z: (row: BarRow) => row.series,\n      fill: (row: BarRow) => {\n        const entry = bySeries.get(row.series);\n        if (entry?.texture === 'dots') return `url(#${texturePrefix}-${series.indexOf(entry)})`;\n        return entry?.fill ?? COLOR_VAR['chart-1'];\n      },\n      radius: {\n        end: (row: BarRow) => bySeries.get(row.series)?.radius ?? cornerRadius,\n        stack: 'each' as const,\n      },\n      layout: stacked ? stack({ order: keys }) : group({ padding: 0, scale: scaleBand(keys, [0, 1]) }),\n      maxThickness: barWidth,\n      states: [\n        {\n          when: { focus: 'unmatched' as const },\n          style: { fillOpacity: fadedOpacity },\n          transition: { type: 'tween' as const, duration: 120, easing: 'ease-out' as const },\n        },\n      ],\n    };\n    const bars = horizontal\n      ? barX(rows, { ...shared, x: 'value', y: 'index' })\n      : barY(rows, { ...shared, x: 'index', y: 'value' });\n    const shaped =\n      (stacked && stackGap > 0) || minBarLength > 0\n        ? withBarLengths(bars, { gap: stacked ? stackGap : 0, floor: minBarLength, horizontal })\n        : bars;\n\n    const stubShared = {\n      id: SKELETON_MARK,\n      key: 'index' as const,\n      fill: 'var(--muted)',\n      radius: { end: cornerRadius },\n      maxThickness: barWidth,\n      motion: false as const,\n    };\n    const stubMark = horizontal\n      ? barX(stubRows, { ...stubShared, x: 'value', y: 'index' })\n      : barY(stubRows, { ...stubShared, x: 'index', y: 'value' });\n\n    const labelOf = (index: number) => {\n      const datum = data[index];\n      if (!datum) return '';\n      return String(datum[xDataKey] ?? '');\n    };\n    const categoryLabel = (index: number) => {\n      const datum = data[index];\n      const format = xAxis?.format;\n      return datum && format ? format(datum, index) : labelOf(index);\n    };\n\n    // A band owns a slice of the axis, with `barGap` of it left empty and split\n    // evenly round the bar, so the end bands sit half a gap from the edge.\n    const categoryScale = scaleBand(indexes, [0, 1]).paddingInner(barGap).paddingOuter(barGap / 2);\n    const categoryStride = xAxis?.ticks ? Math.ceil(bands / Math.max(1, xAxis.ticks)) : 1;\n    const categoryAxisShown = horizontal ? yAxis !== undefined : xAxis !== undefined;\n    const categoryOptions = {\n      scale: categoryScale,\n      axis: categoryAxisShown\n        ? {\n            line: false as const,\n            tickLabels: { opacity: 1 },\n            ticks: {\n              size: 0,\n              format: categoryLabel,\n              ...(xAxis?.ticks && !horizontal\n                ? { values: indexes.filter((index) => index % categoryStride === 0) }\n                : {}),\n            },\n          }\n        : (false as const),\n    };\n\n    const valueTicks = yAxis?.ticks ?? grid?.rows;\n    const valueFormat = (value: number) => {\n      if (loading) return '';\n      const format = yAxis?.format;\n      return format ? format(value) : compactNumber(value);\n    };\n    const valueAxisShown = !horizontal && yAxis !== undefined;\n    const valueOptions = {\n      // While loading the stubs need a scale of their own: their length is a\n      // share of the axis, and there may be no values yet to derive one from.\n      scale: loading\n        ? scaleLinear([0, 1], [0, 1])\n        : yDomain\n          ? scaleLinear(yDomain, [0, 1])\n          : scaleLinear,\n      nice: !loading && !yDomain,\n      grid: grid ? { stroke: 'var(--border)', strokeOpacity: grid.opacity, strokeDasharray: grid.dashArray } : false,\n      axis: valueAxisShown\n        ? {\n            line: false as const,\n            tickLabels: { opacity: 1 },\n            ticks: { size: 0, format: valueFormat, ...(valueTicks ? { count: valueTicks } : {}) },\n          }\n        : horizontal && valueTicks\n          ? ({ line: false as const, ticks: { size: 0, count: valueTicks, format: () => '' } })\n          : (false as const),\n    };\n\n    const interactive = tooltip !== undefined && !loading && rows.length > 0;\n    const readout = tooltip\n      ? {\n          use: chartTooltip,\n          className: tooltip.className,\n          content(points: readonly ChartPoint<BarRow, number, number>[]) {\n            const { formatValue, formatX, labels } = tooltip.latest.current;\n            const first = points[0]?.datum;\n            const datum = first ? data[first.index] : undefined;\n            return {\n              title: datum ? (formatX ? formatX(datum) : labelOf(first!.index)) : '',\n              rows: points.map(({ datum: row, color }) => ({\n                label: labels?.[row.series] ?? row.series,\n                value: formatValue ? formatValue(row.value, row.series) : compactNumber(row.value),\n                color,\n              })),\n            };\n          },\n        }\n      : undefined;\n\n    return defineChart({\n      theme: THEME,\n      marks: loading ? [stubMark] : [shaped],\n      scales: horizontal ? { x: valueOptions, y: categoryOptions } : { x: categoryOptions, y: valueOptions },\n      ...(compact ? { guides: false as const, margin: 0 } : {}),\n      ...(loading && !horizontal && yAxis ? { margin: { left: LOADING_GUTTER } } : {}),\n      focusRing: false,\n      /*\n       * Bars grow in one after another and all finish inside the one duration:\n       * the window each gets is what is left after the last one's delay. Only the\n       * bars arrive, so a replay does not also fade the axis and grid in again.\n       */\n      motion: (context: ChartMotionContext<BarRow>) => {\n        if (context.phase !== 'enter') {\n          return { transition: { type: 'tween' as const, duration: domainDuration, easing: 'ease-out' as const } };\n        }\n        if (context.role !== 'bar') return false as const;\n        const band = context.datum?.index ?? 0;\n        return {\n          delay: bands > 1 ? (band / bands) * animationDuration * STAGGER_SHARE : 0,\n          transition: {\n            type: 'tween' as const,\n            duration: animationDuration * (1 - (bands > 1 ? STAGGER_SHARE : 0)),\n            easing: 'ease-out' as const,\n          },\n        };\n      },\n      keyboard: interactive,\n      pointer: interactive,\n      ...(interactive && readout\n        ? {\n            focus: horizontal ? ('group-y' as const) : ('group-x' as const),\n            maxFocusDistance: Number.POSITIVE_INFINITY,\n            tooltip: readout,\n          }\n        : {}),\n    });\n  }, [\n    data,\n    xDataKey,\n    series,\n    loading,\n    horizontal,\n    stacked,\n    barGap,\n    barWidth,\n    stackGap,\n    cornerRadius,\n    minBarLength,\n    fadedOpacity,\n    yDomain?.[0],\n    yDomain?.[1],\n    compact,\n    animationDuration,\n    domainDuration,\n    grid,\n    xAxis,\n    yAxis,\n    tooltip,\n    skeleton,\n    texturePrefix,\n  ]);\n\n  // Marks take their timing from the definition. This one is what the readout\n  // moves with, and it should keep up with the pointer.\n  const renderer = useMemo(() => motion({ transition: { type: 'tween', duration: 160, easing: 'ease-out' } }), []);\n\n  const summary = label ?? `Bar chart, ${data.length} ${data.length === 1 ? 'row' : 'rows'}`;\n\n  const context = useMemo<BarChartContextValue>(\n    () => ({ data, xDataKey, series, setSeries, removeSeries, setSlot, activeIndex }),\n    [data, xDataKey, series, setSeries, removeSeries, setSlot, activeIndex]\n  );\n\n  const spokenRow = (datum: BarChartDatum, index: number) =>\n    labelForDatum?.(datum, index) ??\n    [xDataKey, ...series.map(({ key }) => key)]\n      .filter((key) => datum[key] !== null && datum[key] !== undefined)\n      .map((key) => `${key}, ${datum[key]}`)\n      .join('. ');\n\n  // The chart also reports a band again when the data is rebuilt under it. Only a\n  // change of band is news, and repeating it can loop a caller that keeps the datum.\n  const reportedIndex = useRef(-1);\n  const reportActive = (index: number) => {\n    if (reportedIndex.current === index) return;\n    reportedIndex.current = index;\n    setActiveIndex(index);\n    onActiveIndexChange?.(index, index >= 0 ? (data[index] ?? null) : null);\n  };\n  const onFocusChange = (point: ChartPoint<BarRow, number, number> | null) =>\n    reportActive(point ? point.datum.index : -1);\n\n  // A remounted chart says nothing about the focus it lost.\n  useEffect(() => reportActive(-1), [revision, status]);\n\n  /*\n   * The chart follows a mouse on its own but not a finger, which it treats as a\n   * tap. A sideways drag reads the bands it crosses, and the last one stays until\n   * the next press, the same as a tap's. A vertical drag never gets here: `pan-y`\n   * gives it to the page, and the browser cancels the pointer.\n   */\n  const readDrag = (event: ReactPointerEvent<HTMLDivElement>) => {\n    if (event.pointerType !== 'touch' || !tooltip || loading) return;\n    const interaction = interactionRef.current;\n    const target = interaction?.resolvePointer(event.clientX, event.clientY);\n    if (!interaction || !target) return;\n    interaction.setControlledFocus(target);\n    if (dragging.current) return;\n    dragging.current = true;\n    document.addEventListener(\n      'pointerdown',\n      () => {\n        dragging.current = false;\n        interactionRef.current?.setControlledFocus(null);\n      },\n      { capture: true, once: true }\n    );\n  };\n\n  const element = useRender({\n    defaultTagName: 'div',\n    render,\n    props: mergeProps(\n      {\n        'data-slot': 'bar-chart',\n        className: slotClasses.root({ className }),\n        children: (\n          <>\n            {loading ? null : (\n              <ul aria-label={summary} className=\"sr-only\">\n                {data.map((datum, index) => (\n                  <li key={index}>{spokenRow(datum, index)}</li>\n                ))}\n              </ul>\n            )}\n            <div\n              ref={plotRef}\n              data-slot=\"bar-chart-plot\"\n              className={slotClasses.plot()}\n              style={{ aspectRatio }}\n              onKeyDown={(event) => {\n                if (event.key === 'Escape') interactionRef.current?.setControlledFocus(null);\n              }}\n              onPointerMove={readDrag}\n              onPointerCancel={() => interactionRef.current?.setControlledFocus(null)}\n            >\n              {width > 0 && series.some((entry) => entry.texture === 'dots') ? (\n                <DotTextures\n                  prefix={texturePrefix}\n                  series={series}\n                  horizontal={horizontal}\n                  width={width}\n                  height={width / aspectRatio}\n                />\n              ) : null}\n              {width > 0 ? (\n                <Chart\n                  // An entrance only grows from the baseline on a chart's first paint,\n                  // so the bars arriving after a load, or a replay, are a new chart.\n                  key={`${revision}:${status}`}\n                  definition={definition}\n                  renderer={renderer}\n                  ariaLabel={summary}\n                  aspectRatio={aspectRatio}\n                  initialWidth={width}\n                  className={slotClasses.chart()}\n                  onFocusChange={onFocusChange}\n                  onRender={({ scene, interaction }) => {\n                    interactionRef.current = interaction;\n                    setStubs(stubOutlines(scene.nodes));\n                  }}\n                />\n              ) : null}\n              {loading && skeleton && stubs ? (\n                <div\n                  data-slot=\"bar-chart-skeleton\"\n                  aria-hidden\n                  className={slotClasses.sweepClip()}\n                  style={{ clipPath: `path('${stubs}')` }}\n                >\n                  <style>{KEYFRAMES}</style>\n                  <div\n                    className={slotClasses.sweep()}\n                    style={{ animation: `panelui-bar-sweep ${skeleton.duration}ms linear infinite` }}\n                  />\n                </div>\n              ) : null}\n            </div>\n            {children}\n          </>\n        ),\n      },\n      props\n    ),\n  });\n\n  return <BarChartContext.Provider value={context}>{element}</BarChartContext.Provider>;\n});\nBarChartRoot.displayName = 'BarChart';\n\n/** Hands a part's formatters to the root without a new function rebuilding the chart. */\nfunction useLatest<T>(value: T) {\n  const [latest] = useState(() => ({ current: value }));\n  useEffect(() => {\n    latest.current = value;\n  });\n  return latest;\n}\n\nexport interface BarChartGridProps {\n  /** Roughly how many lines to draw across the value axis. */\n  rows?: number;\n  /** The stroke's dash pattern. */\n  dashArray?: string;\n  /** Opacity of the lines. */\n  opacity?: number;\n}\n\n/** Lines across the value axis, so a bar can be read against a number. */\nfunction BarChartGrid({ rows = 4, dashArray = '4 6', opacity = 1 }: BarChartGridProps) {\n  const { setSlot } = useChart('BarChart.Grid');\n  useEffect(() => {\n    setSlot('grid', { rows, dashArray, opacity });\n    return () => setSlot('grid', undefined);\n  }, [rows, dashArray, opacity, setSlot]);\n  return null;\n}\nBarChartGrid.displayName = 'BarChart.Grid';\n\n/*\n * The dotted fills, defined once in the page and referenced by the bars.\n *\n * Each is a pattern the size of the plot holding one rectangle in the series'\n * colour, fading from the bars' far end to the baseline and masked by a grid\n * of dots. The fade runs across the whole plot, not each bar, so a taller bar\n * reaches brighter dots, and every bar shares one grid, so the dots line up\n * from one bar to the next.\n */\nfunction DotTextures({\n  prefix,\n  series,\n  horizontal,\n  width,\n  height,\n}: {\n  prefix: string;\n  series: readonly SeriesEntry[];\n  horizontal: boolean;\n  width: number;\n  height: number;\n}) {\n  return (\n    <svg aria-hidden=\"true\" focusable=\"false\" width=\"0\" height=\"0\" className=\"absolute\">\n      <defs>\n        {series.map((entry, index) => {\n          if (entry.texture !== 'dots') return null;\n          const id = `${prefix}-${index}`;\n          const gap = entry.dotGap;\n          return (\n            <g key={entry.key}>\n              <pattern id={`${id}-grid`} width={gap} height={gap} patternUnits=\"userSpaceOnUse\">\n                <circle cx={gap / 2} cy={gap / 2} r={entry.dotSize / 2} fill=\"white\" />\n              </pattern>\n              <mask id={`${id}-mask`} maskUnits=\"userSpaceOnUse\" x={0} y={0} width={width} height={height}>\n                <rect width={width} height={height} fill={`url(#${id}-grid)`} />\n              </mask>\n              {/* Brightest at the end a bar grows towards: the top, or the right when sideways. */}\n              <linearGradient\n                id={`${id}-fade`}\n                gradientUnits=\"userSpaceOnUse\"\n                x1={horizontal ? width : 0}\n                y1={0}\n                x2={0}\n                y2={horizontal ? 0 : height}\n              >\n                {/* As styles, so a colour given as a CSS variable resolves. */}\n                <stop offset={0} style={{ stopColor: entry.fill, stopOpacity: 1 }} />\n                <stop offset={1} style={{ stopColor: entry.fill, stopOpacity: 0.55 }} />\n              </linearGradient>\n              <pattern id={id} width={width} height={height} patternUnits=\"userSpaceOnUse\">\n                {/* A faint wash under the dots, so on a light surface the bar still\n                    reads as a shape and not as loose dots on white. */}\n                <rect width={width} height={height} style={{ fill: entry.fill, fillOpacity: 0.12 }} />\n                <rect width={width} height={height} fill={`url(#${id}-fade)`} mask={`url(#${id}-mask)`} />\n              </pattern>\n            </g>\n          );\n        })}\n      </defs>\n    </svg>\n  );\n}\n\nexport interface BarChartBarProps {\n  /** Column in the data holding this series' values. */\n  dataKey: string;\n  /** Which of the theme's roles the series is drawn in. */\n  color?: BarChartColor;\n  /** Corner radius, overriding the chart's. */\n  cornerRadius?: number;\n  /**\n   * `dots` fills the bars with a grid of dots instead of a solid colour, brighter\n   * towards the end each bar grows to. Use it for a chart that should read as a\n   * printout, or to set one series apart from solid ones. Default `solid`.\n   */\n  texture?: BarChartTexture;\n  /** Diameter of each dot, in pixels, with `texture=\"dots\"`. Default `3`. */\n  dotSize?: number;\n  /** Distance from one dot's centre to the next, in pixels, with `texture=\"dots\"`. Default `5`. */\n  dotGap?: number;\n}\n\n/** One series of bars. Their order is the order they stand in a band or stack. */\nfunction BarChartBar({\n  dataKey,\n  color = 'chart-1',\n  cornerRadius,\n  texture = 'solid',\n  dotSize = 3,\n  dotGap = 5,\n}: BarChartBarProps) {\n  const { setSeries, removeSeries } = useChart('BarChart.Bar');\n  const fill = COLOR_VAR[color];\n  useEffect(() => {\n    setSeries({ key: dataKey, fill, radius: cornerRadius, texture, dotSize: Math.max(0.5, dotSize), dotGap: Math.max(1, dotGap) });\n  }, [dataKey, fill, cornerRadius, texture, dotSize, dotGap, setSeries]);\n  // Apart from the update above, so a new colour keeps the series' place in the order.\n  useEffect(() => () => removeSeries(dataKey), [dataKey, removeSeries]);\n  return null;\n}\nBarChartBar.displayName = 'BarChart.Bar';\n\nexport interface BarChartSkeletonProps {\n  /**\n   * How many placeholder bars to draw. Defaults to one per row, and to seven\n   * when the data has not arrived.\n   */\n  bars?: number;\n  /** Milliseconds for one pass of the sweep. */\n  duration?: number;\n}\n\n/**\n * The loading state: a row of short, equal stubs on the baseline, with a\n * highlight travelling across them. Equal, because stubs of differing heights\n * cannot be told from real data.\n *\n * The highlight is a child translated inside a clip of the stubs' outlines, so\n * the loop runs on the compositor and repaints nothing.\n */\nfunction BarChartSkeleton({ bars, duration = 1400 }: BarChartSkeletonProps) {\n  const { setSlot } = useChart('BarChart.Skeleton');\n  useEffect(() => {\n    setSlot('skeleton', { bars, duration });\n    return () => setSlot('skeleton', undefined);\n  }, [bars, duration, setSlot]);\n  return null;\n}\nBarChartSkeleton.displayName = 'BarChart.Skeleton';\n\nexport interface BarChartXAxisProps {\n  /**\n   * How many labels to show. Every category by default, thinned only when the\n   * bands get too narrow to read. Pass a number to force it lower.\n   */\n  ticks?: number;\n  /** Turn a row into its label. Defaults to the value at `xDataKey`. */\n  format?: (datum: BarChartDatum, index: number) => string;\n}\n\n/** The category labels under the bands. Sideways, `YAxis` has them. */\nfunction BarChartXAxis({ ticks, format }: BarChartXAxisProps) {\n  const { setSlot } = useChart('BarChart.XAxis');\n  useEffect(() => {\n    setSlot('xAxis', { ticks, format });\n    return () => setSlot('xAxis', undefined);\n  }, [ticks, format, setSlot]);\n  return null;\n}\nBarChartXAxis.displayName = 'BarChart.XAxis';\n\nexport interface BarChartYAxisProps {\n  /** Roughly how many labels to show along the value axis. */\n  ticks?: number;\n  /** Format a value for its label. Defaults to a compact number. */\n  format?: (value: number) => string;\n}\n\n/** Value labels down the side. Sideways, the side is the category axis, so the names go there. */\nfunction BarChartYAxis({ ticks = 4, format }: BarChartYAxisProps) {\n  const { setSlot } = useChart('BarChart.YAxis');\n  useEffect(() => {\n    setSlot('yAxis', { ticks, format });\n    return () => setSlot('yAxis', undefined);\n  }, [ticks, format, setSlot]);\n  return null;\n}\nBarChartYAxis.displayName = 'BarChart.YAxis';\n\nexport interface BarChartTooltipProps {\n  /** Format one series' value. Defaults to a compact number. */\n  formatValue?: (value: number, key: string) => string;\n  /** Format the readout's heading from the row. Defaults to the value at xDataKey. */\n  formatX?: (datum: BarChartDatum) => string;\n  /** Prettier names for the series keys, as the legend takes. */\n  labels?: Record<string, string>;\n  /** Added to the readout's surface. */\n  className?: string;\n}\n\n/**\n * The readout, and what drives it: hovering a band, pressing or dragging across\n * them, or the arrow keys. A click or a tap pins the reading until the next\n * press elsewhere or Escape. The whole plot is the hit area, because a readout\n * you have to land on a thin bar to summon is one nobody finds.\n */\nfunction BarChartTooltip({ formatValue, formatX, labels, className }: BarChartTooltipProps) {\n  const { setSlot } = useChart('BarChart.Tooltip');\n  const latest = useLatest({ formatValue, formatX, labels });\n  useEffect(() => {\n    setSlot('tooltip', { className, latest });\n    return () => setSlot('tooltip', undefined);\n  }, [className, latest, setSlot]);\n  return null;\n}\nBarChartTooltip.displayName = 'BarChart.Tooltip';\n\nexport interface BarChartLegendProps extends ComponentProps<'div'> {\n  /** Prettier names for the series keys. */\n  labels?: Record<string, string>;\n}\n\n/** A swatch and a name per series, in the order the series were declared. */\nfunction BarChartLegend({ labels, className, render, ...props }: BarChartLegendProps & RenderProp<'div'>) {\n  const { series } = useChart('BarChart.Legend');\n  const slots = barChartVariants();\n\n  const element = useRender({\n    defaultTagName: 'div',\n    render,\n    props: mergeProps(\n      {\n        'data-slot': 'bar-chart-legend',\n        'aria-hidden': true,\n        className: slots.legend({ className }),\n        children: series.map(({ key, fill: color }) => (\n          <span key={key} className={slots.legendItem()}>\n            <span className={slots.legendSwatch()} style={{ background: color }} />\n            {labels?.[key] ?? key}\n          </span>\n        )),\n      },\n      props\n    ),\n  });\n\n  return series.length ? element : null;\n}\nBarChartLegend.displayName = 'BarChart.Legend';\n\nexport interface BarChartHeaderProps extends Omit<ComponentProps<'div'>, 'title'> {\n  /** Small line above the value: what the chart is of. */\n  title?: ReactNode;\n  /** The readout. The largest thing on the card, and the first thing read. */\n  value?: ReactNode;\n  /** One muted line under the value: a period, a comparison, a total. */\n  caption?: ReactNode;\n  /** Prettier names for the series keys, as the legend takes. */\n  labels?: Record<string, string>;\n  /**\n   * Draw a swatch and a name per series along the trailing edge. Prefer this to\n   * `BarChart.Legend` on a chart that has a header: the legend sits over the\n   * plot, in the corner the bars also want.\n   */\n  legend?: boolean;\n  /** Trailing slot: a control, a badge, a range picker. Wins over `legend`. */\n  children?: ReactNode;\n}\n\n/**\n * The strip above the plot: what the chart is of, what it reads and what the\n * colours mean. The value is not derived here. A readout that follows the\n * pointer takes it from `onActiveIndexChange` and passes the string down, so one\n * header can show a total at rest and a band's value while one is read.\n */\nfunction BarChartHeader({\n  title,\n  value,\n  caption,\n  labels,\n  legend = false,\n  className,\n  children,\n  render,\n  ...props\n}: BarChartHeaderProps & RenderProp<'div'>) {\n  const { series } = useChart('BarChart.Header');\n  const slots = barChartVariants();\n  const trailing =\n    children ??\n    (legend && series.length\n      ? series.map(({ key, fill: color }) => (\n          <span key={key} className={slots.legendItem()}>\n            <span className={slots.legendSwatch()} style={{ background: color }} />\n            {labels?.[key] ?? key}\n          </span>\n        ))\n      : null);\n\n  return useRender({\n    defaultTagName: 'div',\n    render,\n    props: mergeProps(\n      {\n        'data-slot': 'bar-chart-header',\n        className: slots.header({ className }),\n        children: (\n          <>\n            <div className={slots.headerText()}>\n              {title ? <div className={slots.headerMuted()}>{title}</div> : null}\n              {value ? <div className={slots.headerValue()}>{value}</div> : null}\n              {caption ? <div className={slots.headerMuted()}>{caption}</div> : null}\n            </div>\n            {trailing ? <div className={slots.headerTrailing()}>{trailing}</div> : null}\n          </>\n        ),\n      },\n      props\n    ),\n  });\n}\nBarChartHeader.displayName = 'BarChart.Header';\n\nexport const BarChart = Object.assign(BarChartRoot, {\n  Header: BarChartHeader,\n  Grid: BarChartGrid,\n  Bar: BarChartBar,\n  Skeleton: BarChartSkeleton,\n  XAxis: BarChartXAxis,\n  YAxis: BarChartYAxis,\n  Tooltip: BarChartTooltip,\n  Legend: BarChartLegend,\n});\n\n/* By name as well, for a Server Component, which cannot dot into a client\n   module. */\nexport {\n  BarChartHeader,\n  BarChartGrid,\n  BarChartBar,\n  BarChartSkeleton,\n  BarChartXAxis,\n  BarChartYAxis,\n  BarChartTooltip,\n  BarChartLegend,\n};\n"
    }
  ]
}
