{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "area-chart",
  "type": "registry:component",
  "title": "Area Chart",
  "description": "Filled bands over time, stacked or overlaid.",
  "dependencies": [
    "@base-ui/react",
    "@tanstack/charts",
    "d3-shape",
    "tailwind-variants"
  ],
  "devDependencies": [
    "@types/d3-shape"
  ],
  "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/area-chart.tsx",
      "type": "registry:component",
      "target": "@components/panelui/area-chart.tsx",
      "content": "'use client';\n\n/**\n * AreaChart — filled bands over time, stacked or overlaid.\n *\n * ```tsx\n * <AreaChart data={traffic} xDataKey=\"hour\" stacked>\n *   <AreaChart.Grid />\n *   <AreaChart.Area dataKey=\"direct\" />\n *   <AreaChart.Area dataKey=\"search\" color=\"chart-2\" />\n *   <AreaChart.XAxis />\n *   <AreaChart.Tooltip />\n * </AreaChart>\n * ```\n *\n * ## The same parts as the app\n *\n * The parts and their props are the app library's AreaChart, 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 point is read by hovering, tapping or\n * dragging, a click pins the reading, and the arrow keys walk the points.\n *\n * `Line`, `Labels`, `ReferenceLine` and `Marker` are web additions. They are for\n * reading a chart without touching it: a total against a limit, or the values at\n * the end of each series, on a page that shows many charts at once.\n *\n * ## The parts describe, the root draws\n *\n * `Grid`, `Area`, 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 * `Labels` is the exception, because it is text in the page and not part of the\n * drawing. It is in the DOM from the first commit, so the first measurement of\n * the plot already leaves room for it. Reserving that room later would be a\n * resize, and a resize ends the entrance.\n *\n * ## The stack is worked out here\n *\n * The chart's stack layout belongs to the area mark and the line mark has none,\n * so the edge along a band's top would have to repeat it. Each row therefore\n * carries the band's bottom and top already added up, and the area and its edge\n * are both drawn from those. It also means a stack's order is simply the order\n * the `Area` parts were declared in, bottom first.\n *\n * A gap in one series leaves a gap in its own band and counts as zero for the\n * bands above it.\n */\nimport { areaY, d3Curve, defineChart, dot, lineY, ruleX, ruleY, text, type ChartInteractionController, type ChartMotionContext, type ChartPoint, type SceneNode } from '@tanstack/charts';\nimport { crosshair } from '@tanstack/charts/crosshair';\nimport { whenFocused } from '@tanstack/charts/focus/mark';\nimport { decorative } from '@tanstack/charts/mark/decorative';\nimport { motion } from '@tanstack/charts/motion';\nimport { Chart } from '@tanstack/charts/react/core';\nimport { scaleLinear } from '@tanstack/charts/scales/linear';\nimport { tooltip as chartTooltip } from '@tanstack/charts/tooltip';\nimport { curveLinear, curveMonotoneX, curveStepAfter } from 'd3-shape';\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 AreaChartStatus = 'loading' | 'ready';\nexport type AreaChartCurve = 'monotone' | 'linear' | 'step';\nexport type AreaChartDatum = Record<string, string | number | null | undefined>;\n\ntype AreaChartRole =\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  | 'foreground'\n  | 'muted-foreground';\n\n/** A role in the theme, or any CSS colour for one the theme has no role for. */\nexport type AreaChartColor = AreaChartRole | (string & {});\n\nconst COLOR_VAR: Record<AreaChartRole, 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  foreground: 'var(--foreground)',\n  'muted-foreground': 'var(--muted-foreground)',\n};\n\nconst paintOf = (color: AreaChartColor) => COLOR_VAR[color as AreaChartRole] ?? color;\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 CURVES = { monotone: d3Curve(curveMonotoneX), linear: d3Curve(curveLinear), step: d3Curve(curveStepAfter) };\n\nconst areaChartVariants = tv({\n  slots: {\n    root: '@container relative grid w-full grid-cols-[minmax(0,1fr)_auto] 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    legendRule: 'h-0.5 w-2 shrink-0 rounded-full',\n    labels:\n      'col-start-1 row-start-3 m-0 mt-2 flex list-none flex-wrap gap-x-3 gap-y-1 p-0 text-xs @lg:relative @lg:col-start-2 @lg:row-start-2 @lg:mt-0 @lg:block @lg:w-44',\n    labelsItem:\n      'flex items-center gap-1.5 whitespace-nowrap @lg:absolute @lg:inset-x-0 @lg:-translate-y-1/2 @lg:pl-3',\n    labelsAction: 'flex min-w-0 flex-1 cursor-pointer items-center gap-1.5 text-left',\n    labelsName: 'min-w-0 flex-1 truncate font-medium',\n    labelsValue: 'text-muted-foreground tabular-nums',\n    header: 'col-span-full 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 the placeholder band takes. */\nconst SKELETON_BAND = 0.18;\n\nconst SKELETON_MARK = 'area-chart-skeleton';\n\nconst KEYFRAMES = '@keyframes panelui-area-sweep{from{transform:translateX(-100%)}to{transform:translateX(250%)}}';\n\n/** Text size of a rule's label, matching the axes. */\nconst ANNOTATION_FONT_SIZE = 11;\n\n/** How far a dimmed series' label fades, so it stays legible at any band opacity. */\nconst DIMMED_LABEL_OPACITY = 0.45;\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 point of one band: what it reads, and where its band sits there. `value` is null across a gap. */\ninterface BandRow {\n  index: number;\n  series: string;\n  value: number | null;\n  bottom: number;\n  top: number | null;\n}\n\ninterface SeriesEntry {\n  key: string;\n  stroke: string;\n  fillOpacity: number | undefined;\n  gradientToOpacity: number | undefined;\n  opacity: number;\n  showLine: boolean;\n  strokeWidth: number;\n  texture: AreaChartTexture;\n  dotSize: number;\n  dotGap: number;\n}\n\n/** How a band is filled: a smooth wash, or a grid of dots. */\nexport type AreaChartTexture = 'solid' | 'dots';\n\n/** The fill's opacity at the top and the bottom of the plot. */\nconst fillStops = (entry: SeriesEntry, stacked: boolean) => {\n  const dots = entry.texture === 'dots';\n  // Dots cover a fraction of the area, so they carry more opacity than a wash\n  // and keep some at the bottom, where a wash fades out entirely.\n  const top = entry.fillOpacity ?? (dots ? 0.95 : stacked ? 0.6 : 0.35);\n  const bottom = entry.gradientToOpacity ?? (dots ? 0.35 : 0);\n  return { top: top * entry.opacity, bottom: bottom * entry.opacity };\n};\n\ninterface LineEntry {\n  key: string;\n  stroke: string;\n  curve: AreaChartCurve | undefined;\n  dashArray: string | undefined;\n  strokeWidth: number;\n}\n\n/** A rule across the plot: at a value on the `y` axis, or at a row on the `x` axis. */\ninterface Annotation {\n  id: string;\n  axis: 'x' | 'y';\n  at: number;\n  stroke: string;\n  dashArray: string;\n  label: string | undefined;\n}\n\n/** Where the drawing put the value axis, in pixels from the top of the plot's box. */\ninterface ValueAxisPixels {\n  zero: number;\n  unit: number;\n  top: number;\n  bottom: number;\n}\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<AreaChartXAxisProps, 'ticks' | 'format'>;\n  yAxis?: { ticks: number; format: AreaChartYAxisProps['format'] };\n  tooltip?: { className: string | undefined; latest: { current: Pick<AreaChartTooltipProps, 'formatValue' | 'formatX' | 'labels'> } };\n  skeleton?: { duration: number };\n}\n\ninterface AreaChartContextValue {\n  data: readonly AreaChartDatum[];\n  xDataKey: string;\n  stacked: boolean;\n  series: readonly SeriesEntry[];\n  setSeries: (entry: SeriesEntry) => void;\n  removeSeries: (key: string) => void;\n  lines: readonly LineEntry[];\n  setLine: (entry: LineEntry) => void;\n  removeLine: (key: string) => void;\n  setAnnotation: (entry: Annotation) => void;\n  removeAnnotation: (id: string) => void;\n  valueAxis: ValueAxisPixels | null;\n  loading: boolean;\n  setSlot: <K extends keyof Slots>(slot: K, value: Slots[K]) => void;\n  activeIndex: number;\n  onSelect: ((key: string) => void) | undefined;\n}\n\nconst AreaChartContext = createContext<AreaChartContextValue | null>(null);\n\nfunction useChart(component: string): AreaChartContextValue {\n  const context = useContext(AreaChartContext);\n  if (!context) {\n    throw new Error(`${component} must be used within an <AreaChart>`);\n  }\n  return context;\n}\n\n/**\n * The point being read, for something rendered inside the chart. A readout\n * outside it takes `onActiveIndexChange` instead.\n */\nexport function useAreaChart() {\n  const { data, activeIndex, xDataKey } = useChart('useAreaChart');\n  return {\n    activeIndex,\n    activePoint: activeIndex >= 0 ? (data[activeIndex] ?? null) : null,\n    xDataKey,\n  };\n}\n\n/** A cell's value, or null for anything that cannot be drawn. */\nfunction readValue(cell: AreaChartDatum[string]): number | null {\n  return typeof cell === 'number' && !Number.isNaN(cell) ? cell : null;\n}\n\n/** Sits each band on the running total of the ones declared before it, or on zero when overlaid. */\nfunction bandRows(data: readonly AreaChartDatum[], series: readonly SeriesEntry[], stacked: boolean): BandRow[] {\n  return series.flatMap(({ key }, seriesIndex) =>\n    data.map((datum, index) => {\n      const value = readValue(datum[key]);\n      const bottom = stacked\n        ? series.slice(0, seriesIndex).reduce((sum, below) => sum + (readValue(datum[below.key]) ?? 0), 0)\n        : 0;\n      return { index, series: key, value, bottom, top: value === null ? null : bottom + value };\n    })\n  );\n}\n\n/** A line's points, in the bands' shape so the readout can treat them alike. Lines are never stacked. */\nfunction lineRows(data: readonly AreaChartDatum[], lines: readonly LineEntry[]): BandRow[] {\n  return lines.flatMap(({ key }) =>\n    data.map((datum, index) => {\n      const value = readValue(datum[key]);\n      return { index, series: key, value, bottom: 0, top: value };\n    })\n  );\n}\n\n/** The series in reading order: a stack from its top down with the lines above it. */\nfunction keyEntries(series: readonly SeriesEntry[], lines: readonly LineEntry[], stacked: boolean) {\n  const bands = series.map(({ key, stroke }) => ({ key, stroke, rule: false }));\n  const rules = lines.map(({ key, stroke }) => ({ key, stroke, rule: true }));\n  return stacked ? [...rules, ...bands.reverse()] : [...bands, ...rules];\n}\n\n/**\n * Spreads labels apart by at least `gap`: down the column to clear each other, then back\n * up from the bottom if that pushed any past it. A label stays where it is if it has room.\n */\nfunction spreadLabels<Label extends { y: number }>(\n  labels: readonly Label[],\n  gap: number,\n  min: number,\n  max: number\n): Label[] {\n  const placed = [...labels].sort((a, b) => a.y - b.y).map((label) => ({ ...label }));\n  let floor = min;\n  for (const label of placed) {\n    label.y = Math.max(label.y, floor);\n    floor = label.y + gap;\n  }\n  let ceiling = max;\n  for (const label of [...placed].reverse()) {\n    label.y = Math.min(label.y, ceiling);\n    ceiling = label.y - gap;\n  }\n  return placed;\n}\n\n/** The placeholder band's outline from the drawn scene, for the sweep to be clipped to. */\nfunction skeletonOutline(nodes: readonly SceneNode[], x = 0, y = 0, inside = false): string {\n  return nodes\n    .map((node) => {\n      const within = inside || node.key.includes(SKELETON_MARK);\n      if (node.kind === 'group') {\n        return skeletonOutline(node.children, x + (node.translateX ?? 0), y + (node.translateY ?? 0), within);\n      }\n      if (node.kind !== 'area' || !within) return '';\n      return `M${node.points.map(([left, top]) => `${left + x},${top + y}`).join('L')}Z`;\n    })\n    .join('');\n}\n\nexport interface AreaChartProps extends Omit<ComponentProps<'div'>, 'ref' | 'onSelect'> {\n  /** The rows. Each one is a point along the x-axis. */\n  data: readonly AreaChartDatum[];\n  /** Key holding the x label. Used by the axis and the readout. */\n  xDataKey?: string;\n  /**\n   * `loading` holds the bands back and grows them in when it turns `ready`. Add\n   * an `AreaChart.Skeleton` for something to stand in the plot meanwhile.\n   */\n  status?: AreaChartStatus;\n  /** The plot's width ÷ height. `2` is the wide card shape. */\n  aspectRatio?: number;\n  /** Milliseconds for the bands to grow in. */\n  animationDuration?: number;\n  /** Milliseconds for the bands and the axis to settle after the data changes. */\n  domainDuration?: number;\n  /** Fix the value axis instead of deriving it from the data. */\n  yDomain?: [number, number];\n  /**\n   * Sit each band on the running total of the ones below it, so the top edge is\n   * the whole and each thickness is a share. The first `Area` is the bottom of\n   * the stack. Without it the bands overlay and their fills are translucent.\n   */\n  stacked?: boolean;\n  /** `monotone` never overshoots between points. `linear` joins them straight. `step` holds each value until the next. */\n  curve?: AreaChartCurve;\n  /**\n   * The point being read, and `-1`/`null` when there is none. Fires when the\n   * index changes, not per frame.\n   */\n  onActiveIndexChange?: (index: number, datum: AreaChartDatum | null) => void;\n  /** An `Area` was chosen, by pressing it on the plot or its entry in `Labels`. Gets its `dataKey`. */\n  onSelect?: (key: string) => void;\n  /** Drop the axis padding so the bands reach the edges, for a sparkline. */\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: AreaChartDatum, index: number) => string;\n}\n\n/** Imperative handle: re-run the grow-in, for a \"replay\" control. */\nexport interface AreaChartHandle {\n  replay: () => void;\n}\n\nconst AreaChartRoot = forwardRef<AreaChartHandle, AreaChartProps & Omit<RenderProp<'div'>, 'onSelect'>>(function AreaChartRoot(\n  {\n    data,\n    xDataKey = 'date',\n    status = 'ready',\n    aspectRatio = 2,\n    animationDuration = 700,\n    domainDuration = 500,\n    yDomain,\n    stacked = false,\n    curve = 'monotone',\n    onActiveIndexChange,\n    onSelect,\n    compact = false,\n    label,\n    labelForDatum,\n    className,\n    children,\n    render,\n    ...props\n  },\n  ref\n) {\n  const slotClasses = areaChartVariants();\n  const plotRef = useRef<HTMLDivElement>(null);\n  const [width, setWidth] = useState(0);\n  // Pattern ids for dotted bands, unique to this chart so two on a page do not share one.\n  const texturePrefix = `panelui-area-dots-${useId().replace(/[^a-zA-Z0-9_-]/g, '')}`;\n  const [series, setSeriesState] = useState<readonly SeriesEntry[]>([]);\n  const [lines, setLinesState] = useState<readonly LineEntry[]>([]);\n  const [annotations, setAnnotations] = useState<readonly Annotation[]>([]);\n  const [valueAxis, setValueAxis] = useState<ValueAxisPixels | null>(null);\n  const [slots, setSlots] = useState<Slots>({});\n  const [activeIndex, setActiveIndex] = useState(-1);\n  const [revision, setRevision] = useState(0);\n  const [outline, setOutline] = useState('');\n  const interactionRef = useRef<ChartInteractionController<BandRow, number, number> | null>(null);\n  const dragging = useRef(false);\n  // The arrow keys walk rows, not series, so Enter on the plot has no series to mean.\n  const pressedByKey = useRef(false);\n  const dragRelease = useRef<AbortController | null>(null);\n  // A cleanup that only aborted the listener would leave `dragging` set if the\n  // chart is hidden mid-drag (by Activity, say), and no later drag could release.\n  useEffect(\n    () => () => {\n      dragRelease.current?.abort();\n      dragging.current = false;\n    },\n    []\n  );\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 setLine = useCallback(\n    (entry: LineEntry) =>\n      setLinesState((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 removeLine = useCallback(\n    (key: string) => setLinesState((current) => current.filter((entry) => entry.key !== key)),\n    []\n  );\n  const setAnnotation = useCallback(\n    (entry: Annotation) =>\n      setAnnotations((current) =>\n        current.some(({ id }) => id === entry.id)\n          ? current.map((existing) => (existing.id === entry.id ? entry : existing))\n          : [...current, entry]\n      ),\n    []\n  );\n  const removeAnnotation = useCallback(\n    (id: string) => setAnnotations((current) => current.filter((entry) => entry.id !== id)),\n    []\n  );\n  const setSlot = useCallback<AreaChartContextValue['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 { grid, xAxis, yAxis, tooltip, skeleton } = slots;\n\n  const definition = useMemo(() => {\n    const rows = bandRows(data, series, stacked);\n    const ruleRows = lineRows(data, lines);\n    const strokes = new Map([...series, ...lines].map(({ key, stroke }) => [key, stroke]));\n    const gradientId = (key: string) => `panelui-area-${series.findIndex((entry) => entry.key === key)}`;\n    const dotsId = (key: string) => `${texturePrefix}-${series.findIndex((entry) => entry.key === key)}`;\n    const curveFactory = CURVES[curve];\n    const span = Math.max(1, data.length - 1);\n\n    const area = areaY(rows, {\n      id: 'area-chart-bands',\n      x: 'index',\n      y1: 'bottom',\n      y2: 'top',\n      z: 'series',\n      key: 'index',\n      fill: (row: BandRow) =>\n        series.find((entry) => entry.key === row.series)?.texture === 'dots'\n          ? `url(#${dotsId(row.series)})`\n          : `url(#${gradientId(row.series)})`,\n      fillOpacity: 1,\n      curve: curveFactory,\n    });\n    // One mark per series, because a mark has one stroke width.\n    const edges = series\n      .filter(({ showLine }) => showLine)\n      .map(({ key, stroke, opacity, strokeWidth }) =>\n        decorative(\n          lineY(\n            rows.filter((row) => row.series === key),\n            {\n              id: `area-chart-edge-${key}`,\n              x: 'index',\n              y: 'top',\n              key: 'index',\n              stroke,\n              strokeOpacity: opacity,\n              strokeWidth,\n              lineCap: 'round',\n              lineJoin: 'round',\n              curve: curveFactory,\n            }\n          )\n        )\n      );\n    // Not decorative, unlike the edges, so its value is in the readout. One mark per\n    // line, because each has its own curve and dash.\n    const lineMarks = lines.map(({ key, stroke, curve: lineCurve, dashArray, strokeWidth }) =>\n      lineY(\n        ruleRows.filter((row) => row.series === key),\n        {\n          id: `area-chart-line-${key}`,\n          x: 'index',\n          y: 'top',\n          key: 'index',\n          stroke,\n          strokeWidth,\n          strokeDasharray: dashArray,\n          lineCap: 'butt',\n          lineJoin: 'round',\n          curve: lineCurve ? CURVES[lineCurve] : curveFactory,\n        }\n      )\n    );\n    // The top of what is drawn, for a marker's label. The derived axis rounds up from\n    // here, so the label is inside the plot without having to know the rounded number.\n    const peak = Math.max(\n      ...[...rows, ...ruleRows].map(({ top }) => top ?? 0),\n      ...annotations.filter(({ axis }) => axis === 'y').map(({ at }) => at)\n    );\n    const annotationMarks = annotations.flatMap(({ id, axis, at, stroke, dashArray, label: name }) => {\n      const nearEnd = axis === 'x' && at > span * 0.75;\n      return [\n        decorative(\n          axis === 'y'\n            ? ruleY([at], { id: `area-chart-rule-${id}`, stroke, strokeDasharray: dashArray, strokeWidth: 1 })\n            : ruleX([at], { id: `area-chart-rule-${id}`, stroke, strokeDasharray: dashArray, strokeWidth: 1 })\n        ),\n        ...(name\n          ? [\n              decorative(\n                text([name], {\n                  id: `area-chart-rule-label-${id}`,\n                  x: () => (axis === 'y' ? 0 : at),\n                  y: () => (axis === 'y' ? at : yDomain?.[1] ?? peak),\n                  text: () => name,\n                  fill: stroke,\n                  fontSize: ANNOTATION_FONT_SIZE,\n                  anchor: nearEnd ? 'end' : 'start',\n                  dx: nearEnd ? -6 : axis === 'y' ? 8 : 6,\n                  dy: axis === 'y' ? -8 : 8,\n                })\n              ),\n            ]\n          : []),\n      ];\n    });\n    // A dot on every band's top edge and every line, for the point being read.\n    const dots = [...series, ...lines].map(({ key, stroke }) =>\n      whenFocused(\n        dot(\n          [...rows, ...ruleRows].filter((row) => row.series === key && row.top !== null),\n          {\n            id: `area-chart-dot-${key}`,\n            x: 'index',\n            y: 'top',\n            key: 'index',\n            r: 4.5,\n            fill: stroke,\n            stroke: 'var(--card)',\n            strokeWidth: 2,\n          }\n        ),\n        { match: 'x' }\n      )\n    );\n    const guide = crosshair<number, number>({ x: { stroke: 'var(--muted-foreground)', strokeOpacity: 0.6 }, y: false });\n\n    const showBand = loading && skeleton !== undefined;\n    const placeholderRows: BandRow[] = showBand\n      ? [0, span].map((index) => ({ index, series: '', value: SKELETON_BAND, bottom: 0, top: SKELETON_BAND }))\n      : [];\n    const placeholder = areaY(placeholderRows, {\n      id: SKELETON_MARK,\n      x: 'index',\n      y: 'top',\n      key: 'index',\n      fill: 'var(--muted)',\n      fillOpacity: 1,\n      motion: false,\n    });\n\n    const labelOf = (index: number) => {\n      const datum = data[index];\n      return datum ? String(datum[xDataKey] ?? '') : '';\n    };\n    const pointLabel = (index: number) => {\n      const datum = data[index];\n      const format = xAxis?.format;\n      return datum && format ? format(datum, index) : labelOf(index);\n    };\n\n    // Evenly spaced with both ends kept, so the last row always has its label.\n    const indexes = data.map((_row, index) => index);\n    const tickValues =\n      xAxis?.ticks && xAxis.ticks >= 2 && xAxis.ticks < data.length\n        ? Array.from({ length: xAxis.ticks }, (_unused, tick) => Math.round((tick * span) / (xAxis.ticks! - 1)))\n        : indexes;\n    const xOptions = {\n      scale: scaleLinear([0, span], [0, 1]),\n      axis: xAxis\n        ? {\n            line: false as const,\n            tickLabels: { opacity: 1 },\n            ticks: { size: 0, format: pointLabel, values: tickValues },\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 yOptions = {\n      // While loading the band needs a scale of its own: its height is a share\n      // of the axis, and there may be no values yet to derive one from.\n      scale: loading ? scaleLinear([0, 1], [0, 1]) : yDomain ? scaleLinear(yDomain, [0, 1]) : scaleLinear,\n      nice: !loading && !yDomain,\n      grid: grid ? { stroke: 'var(--border)', strokeOpacity: grid.opacity, strokeDasharray: grid.dashArray } : false,\n      axis: yAxis\n        ? {\n            line: false as const,\n            tickLabels: { opacity: 1 },\n            ticks: { size: 0, format: valueFormat, ...(valueTicks ? { count: valueTicks } : {}) },\n          }\n        : (false as const),\n    };\n\n    const drawn = [...rows, ...ruleRows].some(({ value }) => value !== null);\n    const interactive = tooltip !== undefined && !loading && drawn;\n    const readout = tooltip\n      ? {\n          use: chartTooltip,\n          className: tooltip.className,\n          // Beside the crosshair and along the top, clear of the dots it is reading.\n          anchor: { x: 'value', y: 'plot-top' } as const,\n          placement: ['bottom-right', 'bottom-left'] as const,\n          offset: 12,\n          content(points: readonly ChartPoint<BandRow, 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            const order = keyEntries(series, lines, stacked).map(({ key }) => key);\n            const listed = [...points].sort(\n              (a, b) => order.indexOf(a.datum.series) - order.indexOf(b.datum.series)\n            );\n            return {\n              title: datum ? (formatX ? formatX(datum) : labelOf(first!.index)) : '',\n              rows: listed.map(({ datum: row }) => ({\n                label: labels?.[row.series] ?? row.series,\n                value: formatValue ? formatValue(row.value ?? 0, row.series) : compactNumber(row.value ?? 0),\n                color: strokes.get(row.series),\n              })),\n            };\n          },\n        }\n      : undefined;\n\n    return defineChart({\n      theme: THEME,\n      // Bottom of the stack first, and a later band over an earlier one.\n      marks: loading\n        ? [placeholder]\n        : [area, ...edges, ...lineMarks, ...annotationMarks, ...(interactive ? [guide, ...dots] : [])],\n      scales: { x: xOptions, y: yOptions },\n      gradients: series.map((entry) => ({\n        id: gradientId(entry.key),\n        x1: 0,\n        y1: 0,\n        x2: 0,\n        y2: 1,\n        stops: [\n          // Stacked bands touch and carry more weight. Overlaid ones have to show each other through.\n          { offset: 0, color: entry.stroke, opacity: fillStops(entry, stacked).top },\n          { offset: 1, color: entry.stroke, opacity: fillStops(entry, stacked).bottom },\n        ],\n      })),\n      ...(compact ? { guides: false as const, margin: 0 } : {}),\n      ...(loading && yAxis ? { margin: { left: LOADING_GUTTER } } : {}),\n      focusRing: false,\n      // Only the bands arrive, so a replay does not also fade the axis and grid in again.\n      motion: (context: ChartMotionContext<BandRow>) => {\n        if (context.phase !== 'enter') {\n          return { transition: { type: 'tween' as const, duration: domainDuration, easing: 'ease-out' as const } };\n        }\n        if (context.role !== 'area' && context.role !== 'line') return false as const;\n        return { transition: { type: 'tween' as const, duration: animationDuration, easing: 'ease-out' as const } };\n      },\n      keyboard: interactive,\n      pointer: interactive,\n      ...(interactive && readout\n        ? {\n            focus: 'group-x' as const,\n            maxFocusDistance: Number.POSITIVE_INFINITY,\n            tooltip: readout,\n          }\n        : {}),\n    });\n  }, [\n    data,\n    xDataKey,\n    series,\n    lines,\n    annotations,\n    loading,\n    stacked,\n    curve,\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 ?? `Area chart, ${data.length} ${data.length === 1 ? 'row' : 'rows'}`;\n\n  const context = useMemo<AreaChartContextValue>(\n    () => ({\n      data,\n      xDataKey,\n      stacked,\n      series,\n      setSeries,\n      removeSeries,\n      lines,\n      setLine,\n      removeLine,\n      setAnnotation,\n      removeAnnotation,\n      valueAxis,\n      loading,\n      setSlot,\n      activeIndex,\n      onSelect,\n    }),\n    [\n      onSelect,\n      data,\n      xDataKey,\n      stacked,\n      series,\n      setSeries,\n      removeSeries,\n      lines,\n      setLine,\n      removeLine,\n      setAnnotation,\n      removeAnnotation,\n      valueAxis,\n      loading,\n      setSlot,\n      activeIndex,\n    ]\n  );\n\n  const spokenRow = (datum: AreaChartDatum, index: number) =>\n    labelForDatum?.(datum, index) ??\n    [xDataKey, ...series.map(({ key }) => key), ...lines.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 point again when the data is rebuilt under it. Only a\n  // change of point 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<BandRow, 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 points 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    dragRelease.current = new AbortController();\n    document.addEventListener(\n      'pointerdown',\n      () => {\n        dragging.current = false;\n        interactionRef.current?.setControlledFocus(null);\n      },\n      { capture: true, once: true, signal: dragRelease.current.signal }\n    );\n  };\n\n  const element = useRender({\n    defaultTagName: 'div',\n    render,\n    props: mergeProps(\n      {\n        'data-slot': 'area-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=\"area-chart-plot\"\n              className={slotClasses.plot()}\n              style={{ aspectRatio }}\n              onPointerDownCapture={() => {\n                pressedByKey.current = false;\n              }}\n              onKeyDownCapture={() => {\n                pressedByKey.current = true;\n              }}\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                  stacked={stacked}\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 bands 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                  onSelect={\n                    onSelect\n                      ? (point) => {\n                          const key = point?.datum.series;\n                          if (key && !pressedByKey.current && series.some((entry) => entry.key === key)) onSelect(key);\n                        }\n                      : undefined\n                  }\n                  onRender={({ scene, interaction }) => {\n                    interactionRef.current = interaction;\n                    setOutline(skeletonOutline(scene.nodes));\n                    const scale = scene.scales.y;\n                    if (!scale) return;\n                    const zero = scale.map(0);\n                    const unit = scale.map(1) - zero;\n                    const top = scene.chart.y;\n                    const bottom = top + scene.chart.height;\n                    setValueAxis((current) =>\n                      current &&\n                      current.zero === zero &&\n                      current.unit === unit &&\n                      current.top === top &&\n                      current.bottom === bottom\n                        ? current\n                        : { zero, unit, top, bottom }\n                    );\n                  }}\n                />\n              ) : null}\n              {loading && skeleton && outline ? (\n                <div\n                  data-slot=\"area-chart-skeleton\"\n                  aria-hidden\n                  className={slotClasses.sweepClip()}\n                  style={{ clipPath: `path('${outline}')` }}\n                >\n                  <style>{KEYFRAMES}</style>\n                  <div\n                    className={slotClasses.sweep()}\n                    style={{ animation: `panelui-area-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 <AreaChartContext.Provider value={context}>{element}</AreaChartContext.Provider>;\n});\nAreaChartRoot.displayName = 'AreaChart';\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 AreaChartGridProps {\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 band can be read against a number. */\nfunction AreaChartGrid({\n  rows = 4,\n  dashArray = '4 6',\n  opacity = 1,\n}: AreaChartGridProps) {\n  const { setSlot } = useChart('AreaChart.Grid');\n  useEffect(() => {\n    setSlot('grid', { rows, dashArray, opacity });\n    return () => setSlot('grid', undefined);\n  }, [rows, dashArray, opacity, setSlot]);\n  return null;\n}\nAreaChartGrid.displayName = 'AreaChart.Grid';\n\n/*\n * The dotted fills, defined once in the page and referenced by the bands.\n *\n * A band's fill is a pattern the size of the plot holding one rectangle: the\n * series' colour fading from the top of the plot to the bottom, masked by a\n * grid of dots. The fade is in the plot's coordinates rather than the band's,\n * so every column's dots get brighter towards the top of the chart, and two\n * dotted bands share one grid, so their dots line up where they overlap.\n */\nfunction DotTextures({\n  prefix,\n  series,\n  stacked,\n  width,\n  height,\n}: {\n  prefix: string;\n  series: readonly SeriesEntry[];\n  stacked: 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 { top, bottom } = fillStops(entry, stacked);\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              <linearGradient id={`${id}-fade`} gradientUnits=\"userSpaceOnUse\" x1={0} y1={0} x2={0} y2={height}>\n                {/* As styles, so a colour given as a CSS variable resolves. */}\n                <stop offset={0} style={{ stopColor: entry.stroke, stopOpacity: top }} />\n                <stop offset={1} style={{ stopColor: entry.stroke, stopOpacity: bottom }} />\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 band still\n                    reads as a shape and not as loose dots on white. */}\n                <rect width={width} height={height} style={{ fill: entry.stroke, fillOpacity: 0.08 * entry.opacity }} />\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 AreaChartAreaProps {\n  /** Column in the data holding this series' values. */\n  dataKey: string;\n  /** A role in the theme, or any CSS colour. */\n  color?: AreaChartColor;\n  /**\n   * Opacity of the fill at the top of the band. Defaults to `0.6` stacked and\n   * `0.35` overlaid, so overlaid bands show each other through.\n   */\n  fillOpacity?: number;\n  /** Opacity at the bottom. `0` fades the band out; match `fillOpacity` for a flat fill. */\n  gradientToOpacity?: number;\n  /** Opacity of the whole series, fill and edge, for picking one out from the others. */\n  opacity?: number;\n  /** Draw the line along the top edge of the band. */\n  showLine?: boolean;\n  /** Thickness of that line. */\n  strokeWidth?: number;\n  /**\n   * `dots` fills the band with a grid of dots instead of a wash, brightest at\n   * the top of the plot. Use it for one series picked out from the others, or\n   * for a chart that should read as a printout rather than a shape. Default `solid`.\n   */\n  texture?: AreaChartTexture;\n  /** Diameter of each dot, in pixels, with `texture=\"dots\"`. Default `2.5`. */\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/**\n * One filled band with a line along its top edge. The fill fades downward,\n * because a flat fill of any weight competes with the line, and the top edge is\n * the part carrying the numbers. The first one declared is the bottom of a stack.\n */\nfunction AreaChartArea({\n  dataKey,\n  color = 'chart-1',\n  fillOpacity,\n  gradientToOpacity,\n  opacity = 1,\n  showLine = true,\n  strokeWidth = 2,\n  texture = 'solid',\n  dotSize = 2.5,\n  dotGap = 5,\n}: AreaChartAreaProps) {\n  const { setSeries, removeSeries } = useChart('AreaChart.Area');\n  const stroke = paintOf(color);\n  useEffect(() => {\n    setSeries({\n      key: dataKey,\n      stroke,\n      fillOpacity,\n      gradientToOpacity,\n      opacity,\n      showLine,\n      strokeWidth,\n      texture,\n      dotSize: Math.max(0.5, dotSize),\n      dotGap: Math.max(1, dotGap),\n    });\n  }, [dataKey, stroke, fillOpacity, gradientToOpacity, opacity, showLine, strokeWidth, 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}\nAreaChartArea.displayName = 'AreaChart.Area';\n\nexport interface AreaChartLineProps {\n  /** Column in the data holding this line's values. */\n  dataKey: string;\n  /** A role in the theme, or any CSS colour. */\n  color?: AreaChartColor;\n  /** Defaults to the chart's `curve`. `step` suits a value that changes on set dates, such as income. */\n  curve?: AreaChartCurve;\n  /** The stroke's dash pattern. */\n  dashArray?: string;\n  /** Thickness of the line. */\n  strokeWidth?: number;\n}\n\n/**\n * A series drawn as a line with no fill, over the bands. It is never part of a\n * stack, so it can show a total the stack is measured against. It counts toward\n * the value axis and has its own row in the readout.\n */\nfunction AreaChartLine({\n  dataKey,\n  color = 'foreground',\n  curve,\n  dashArray,\n  strokeWidth = 2,\n}: AreaChartLineProps) {\n  const { setLine, removeLine } = useChart('AreaChart.Line');\n  const stroke = paintOf(color);\n  useEffect(() => {\n    setLine({ key: dataKey, stroke, curve, dashArray, strokeWidth });\n  }, [dataKey, stroke, curve, dashArray, strokeWidth, setLine]);\n  useEffect(() => () => removeLine(dataKey), [dataKey, removeLine]);\n  return null;\n}\nAreaChartLine.displayName = 'AreaChart.Line';\n\n/** Registers one rule with the root, keeping its place in the order when its props change. */\nfunction useAnnotation(component: string, entry: Omit<Annotation, 'id'>) {\n  const { setAnnotation, removeAnnotation } = useChart(component);\n  const id = useId();\n  const { axis, at, stroke, dashArray, label } = entry;\n  useEffect(() => {\n    setAnnotation({ id, axis, at, stroke, dashArray, label });\n  }, [id, axis, at, stroke, dashArray, label, setAnnotation]);\n  useEffect(() => () => removeAnnotation(id), [id, removeAnnotation]);\n}\n\nexport interface AreaChartReferenceLineProps {\n  /** The value to draw the rule at. It counts toward the value axis. */\n  y: number;\n  /** A role in the theme, or any CSS colour, for the rule and its label. */\n  color?: AreaChartColor;\n  /** The stroke's dash pattern. */\n  dashArray?: string;\n  /** Text above the rule's left end. */\n  label?: string;\n}\n\n/** A rule across the plot at a fixed value: a budget, a limit, a target. */\nfunction AreaChartReferenceLine({\n  y,\n  color = 'foreground',\n  dashArray = '3 4',\n  label,\n}: AreaChartReferenceLineProps) {\n  useAnnotation('AreaChart.ReferenceLine', { axis: 'y', at: y, stroke: paintOf(color), dashArray, label });\n  return null;\n}\nAreaChartReferenceLine.displayName = 'AreaChart.ReferenceLine';\n\nexport interface AreaChartMarkerProps {\n  /** The row to draw the rule at, counted from `0`. */\n  index: number;\n  /** A role in the theme, or any CSS colour, for the rule and its label. */\n  color?: AreaChartColor;\n  /** The stroke's dash pattern. */\n  dashArray?: string;\n  /** Text beside the rule's top. It moves to the left of a rule near the right edge. */\n  label?: string;\n}\n\n/** A rule down the plot at one row: a release, a date something ran out. */\nfunction AreaChartMarker({\n  index,\n  color = 'muted-foreground',\n  dashArray = '2 3',\n  label,\n}: AreaChartMarkerProps) {\n  useAnnotation('AreaChart.Marker', { axis: 'x', at: index, stroke: paintOf(color), dashArray, label });\n  return null;\n}\nAreaChartMarker.displayName = 'AreaChart.Marker';\n\n/** A label of your own in the `Labels` column, with no swatch. */\nexport interface AreaChartExtraLabel {\n  /** Identifies the label among the others. */\n  id: string;\n  /** The label's name. */\n  label: string;\n  /** The value it shows. */\n  value: number;\n  /** The place on the value axis to sit beside, which can differ from `value`. */\n  at: number;\n}\n\nexport interface AreaChartLabelsProps extends ComponentProps<'ol'> {\n  /** Prettier names for the series keys. */\n  labels?: Record<string, string>;\n  /** Format a value for its label. Defaults to a compact number. */\n  formatValue?: (value: number, key: string) => string;\n  /** Labels of your own, for a figure that belongs to no one series. */\n  extra?: readonly AreaChartExtraLabel[];\n  /** The least distance between two labels, in pixels. */\n  minGap?: number;\n}\n\n/**\n * The value at the end of each series, beside where that series ends, so a chart\n * can be read without hovering it. Labels that would touch are spread apart. Below\n * 32rem of width the column would take too much of the plot, so they wrap under it\n * instead.\n */\nfunction AreaChartLabels({\n  labels,\n  formatValue,\n  extra,\n  minGap = 16,\n  className,\n  render,\n  ...props\n}: AreaChartLabelsProps & RenderProp<'ol'>) {\n  const { data, series, lines, stacked, valueAxis, loading, onSelect } = useChart('AreaChart.Labels');\n  const slots = areaChartVariants();\n  const last = data.slice(-1);\n  const opacities = new Map(series.map(({ key, opacity }) => [key, opacity]));\n  const swatchOf = new Map(keyEntries(series, lines, stacked).map((entry) => [entry.key, entry]));\n\n  const pixel = (value: number) => (valueAxis ? valueAxis.zero + value * valueAxis.unit : 0);\n  const entries = [\n    ...lineRows(last, lines).map(({ series: key, value, top }) => ({ key, value, y: pixel(top ?? 0) })),\n    // Beside the middle of a band, which is where its colour is.\n    ...bandRows(last, series, stacked).map(({ series: key, value, bottom, top }) => ({\n      key,\n      value,\n      y: pixel((bottom + (top ?? bottom)) / 2),\n    })),\n    ...(extra ?? []).map(({ id, value, at }) => ({ key: id, value, y: pixel(at) })),\n  ].filter(({ value }) => value !== null);\n  const placed =\n    valueAxis && !loading ? spreadLabels(entries, minGap, valueAxis.top, valueAxis.bottom) : [];\n  const names = new Map((extra ?? []).map(({ id, label }) => [id, label]));\n\n  return useRender({\n    defaultTagName: 'ol',\n    render,\n    props: mergeProps(\n      {\n        'data-slot': 'area-chart-labels',\n        className: slots.labels({ className }),\n        children: placed.map(({ key, value, y }) => {\n          const swatch = swatchOf.get(key);\n          const content = (\n            <>\n              <span\n                aria-hidden\n                className={swatch?.rule ? slots.legendRule() : slots.legendSwatch()}\n                style={{ background: swatch?.stroke ?? 'transparent' }}\n              />\n              <span className={slots.labelsName()}>{names.get(key) ?? labels?.[key] ?? key}</span>\n              <span className={slots.labelsValue()}>\n                {formatValue ? formatValue(value ?? 0, key) : compactNumber(value ?? 0)}\n              </span>\n            </>\n          );\n          return (\n            <li\n              key={key}\n              className={slots.labelsItem()}\n              style={{ top: y, opacity: (opacities.get(key) ?? 1) < 1 ? DIMMED_LABEL_OPACITY : undefined }}\n            >\n              {onSelect && opacities.has(key) ? (\n                <button type=\"button\" className={slots.labelsAction()} onClick={() => onSelect(key)}>\n                  {content}\n                </button>\n              ) : (\n                content\n              )}\n            </li>\n          );\n        }),\n      },\n      props\n    ),\n  });\n}\nAreaChartLabels.displayName = 'AreaChart.Labels';\n\nexport interface AreaChartSkeletonProps {\n  /** Milliseconds for one pass of the sweep. */\n  duration?: number;\n}\n\n/**\n * The loading state: a low band along the baseline with a highlight travelling\n * across it.\n *\n * The highlight is a child translated inside a clip of the band's outline, so\n * the loop runs on the compositor and repaints nothing.\n */\nfunction AreaChartSkeleton({\n  duration = 1400,\n}: AreaChartSkeletonProps) {\n  const { setSlot } = useChart('AreaChart.Skeleton');\n  useEffect(() => {\n    setSlot('skeleton', { duration });\n    return () => setSlot('skeleton', undefined);\n  }, [duration, setSlot]);\n  return null;\n}\nAreaChartSkeleton.displayName = 'AreaChart.Skeleton';\n\nexport interface AreaChartXAxisProps {\n  /**\n   * How many labels to show, evenly spaced with both ends kept. Every row by\n   * default, thinned only when they get too close to read.\n   */\n  ticks?: number;\n  /** Turn a row into its label. Defaults to the value at `xDataKey`. */\n  format?: (datum: AreaChartDatum, index: number) => string;\n}\n\n/** The x labels, each placed on its own point. */\nfunction AreaChartXAxis({\n  ticks,\n  format,\n}: AreaChartXAxisProps) {\n  const { setSlot } = useChart('AreaChart.XAxis');\n  useEffect(() => {\n    setSlot('xAxis', { ticks, format });\n    return () => setSlot('xAxis', undefined);\n  }, [ticks, format, setSlot]);\n  return null;\n}\nAreaChartXAxis.displayName = 'AreaChart.XAxis';\n\nexport interface AreaChartYAxisProps {\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, with room made for them. */\nfunction AreaChartYAxis({\n  ticks = 4,\n  format,\n}: AreaChartYAxisProps) {\n  const { setSlot } = useChart('AreaChart.YAxis');\n  useEffect(() => {\n    setSlot('yAxis', { ticks, format });\n    return () => setSlot('yAxis', undefined);\n  }, [ticks, format, setSlot]);\n  return null;\n}\nAreaChartYAxis.displayName = 'AreaChart.YAxis';\n\nexport interface AreaChartTooltipProps {\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: AreaChartDatum) => 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 the plot, pressing or dragging across\n * it, or the arrow keys. It brings the crosshair and a dot on every band's top\n * edge. A click or a tap pins the reading until the next press elsewhere or\n * Escape.\n */\nfunction AreaChartTooltip({ formatValue, formatX, labels, className }: AreaChartTooltipProps) {\n  const { setSlot } = useChart('AreaChart.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}\nAreaChartTooltip.displayName = 'AreaChart.Tooltip';\n\nexport interface AreaChartLegendProps 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. A stack reads from its top down, the order the bands appear, with lines first. */\nfunction AreaChartLegend({ labels, className, render, ...props }: AreaChartLegendProps & RenderProp<'div'>) {\n  const { series, lines, stacked } = useChart('AreaChart.Legend');\n  const listed = keyEntries(series, lines, stacked);\n  const slots = areaChartVariants();\n\n  const element = useRender({\n    defaultTagName: 'div',\n    render,\n    props: mergeProps(\n      {\n        'data-slot': 'area-chart-legend',\n        'aria-hidden': true,\n        className: slots.legend({ className }),\n        children: listed.map(({ key, stroke, rule }) => (\n          <span key={key} className={slots.legendItem()}>\n            <span className={rule ? slots.legendRule() : slots.legendSwatch()} style={{ background: stroke }} />\n            {labels?.[key] ?? key}\n          </span>\n        )),\n      },\n      props\n    ),\n  });\n\n  return listed.length ? element : null;\n}\nAreaChartLegend.displayName = 'AreaChart.Legend';\n\nexport interface AreaChartHeaderProps 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   * `AreaChart.Legend` on a chart that has a header: the legend sits over the\n   * plot, in the corner the bands 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 point's value while one is read.\n */\nfunction AreaChartHeader({\n  title,\n  value,\n  caption,\n  labels,\n  legend = false,\n  className,\n  children,\n  render,\n  ...props\n}: AreaChartHeaderProps & RenderProp<'div'>) {\n  const { series, lines, stacked } = useChart('AreaChart.Header');\n  const listed = keyEntries(series, lines, stacked);\n  const slots = areaChartVariants();\n  const trailing =\n    children ??\n    (legend && listed.length\n      ? listed.map(({ key, stroke, rule }) => (\n          <span key={key} className={slots.legendItem()}>\n            <span className={rule ? slots.legendRule() : slots.legendSwatch()} style={{ background: stroke }} />\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': 'area-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 === 0 || 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}\nAreaChartHeader.displayName = 'AreaChart.Header';\n\nexport const AreaChart = Object.assign(AreaChartRoot, {\n  Header: AreaChartHeader,\n  Grid: AreaChartGrid,\n  Area: AreaChartArea,\n  Line: AreaChartLine,\n  ReferenceLine: AreaChartReferenceLine,\n  Marker: AreaChartMarker,\n  Labels: AreaChartLabels,\n  Skeleton: AreaChartSkeleton,\n  XAxis: AreaChartXAxis,\n  YAxis: AreaChartYAxis,\n  Tooltip: AreaChartTooltip,\n  Legend: AreaChartLegend,\n});\n\n/* By name as well, for a Server Component, which cannot dot into a client\n   module. */\nexport {\n  AreaChartHeader,\n  AreaChartGrid,\n  AreaChartArea,\n  AreaChartLine,\n  AreaChartReferenceLine,\n  AreaChartMarker,\n  AreaChartLabels,\n  AreaChartSkeleton,\n  AreaChartXAxis,\n  AreaChartYAxis,\n  AreaChartTooltip,\n  AreaChartLegend,\n};\n"
    }
  ]
}
