{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "line-chart",
  "title": "LineChart",
  "description": "A trend over time. Point at it, or focus it and press the arrow keys.",
  "registryDependencies": [
    "https://creance.pages.dev/r/creance-base.json",
    "https://creance.pages.dev/r/creance-chart.json"
  ],
  "files": [
    {
      "path": "line-chart/LineChart.css",
      "content": ".cr-line-chart__plot {\n  border-radius: var(--cr-radius-sm);\n  cursor: crosshair;\n  touch-action: pan-y;\n}\n\n.cr-line-chart__plot:focus-visible {\n  outline: var(--cr-focus-width) solid var(--cr-color-focus);\n  outline-offset: var(--cr-focus-offset);\n}\n\n.cr-line-chart__svg {\n  position: absolute;\n  inset: 0;\n  display: block;\n  width: 100%;\n  height: 100%;\n  overflow: visible;\n}\n\n.cr-line-chart__line {\n  fill: none;\n  stroke-width: 2.2;\n  stroke-linecap: round;\n  stroke-linejoin: round;\n  vector-effect: non-scaling-stroke;\n}\n\n.cr-line-chart__line[data-style='dashed'] {\n  stroke-width: 1.6;\n  stroke-dasharray: 5 5;\n}\n\n.cr-line-chart__rule {\n  position: absolute;\n  top: 0;\n  bottom: 0;\n  width: 0;\n  border-inline-start: 1px dashed var(--cr-chart-2);\n  pointer-events: none;\n}\n\n.cr-line-chart__marker {\n  position: absolute;\n  width: 7px;\n  height: 7px;\n  border-radius: var(--cr-radius-full);\n  background: currentColor;\n  transform: translate(-50%, -50%);\n  pointer-events: none;\n}\n\n.cr-line-chart__marker[data-main] {\n  box-sizing: border-box;\n  width: 11px;\n  height: 11px;\n  border: 2px solid currentColor;\n  background: var(--cr-color-bg-raised);\n}\n\n.cr-line-chart__tip {\n  position: absolute;\n  bottom: var(--cr-space-2);\n  z-index: 2;\n  display: flex;\n  flex-direction: column;\n  gap: 6px;\n  min-width: 132px;\n  margin-inline: var(--cr-space-3);\n  padding: var(--cr-space-2) var(--cr-space-3);\n  border-radius: 14px;\n  font-size: var(--cr-font-size-xs);\n  white-space: nowrap;\n  pointer-events: none;\n}\n\n/* Past the middle, the tooltip sits to the left of the rule so it stays inside the plot. */\n.cr-line-chart__tip[data-side='left'] {\n  transform: translateX(calc(-100% - 2 * var(--cr-space-3)));\n}\n\n.cr-line-chart__tip-title {\n  font-weight: var(--cr-font-weight-semibold);\n}\n\n.cr-line-chart__tip .cr-chart-legend {\n  flex-direction: column;\n  gap: var(--cr-space-1);\n  font-size: var(--cr-font-size-xs);\n}\n\n.cr-line-chart__label {\n  position: absolute;\n  top: 2px;\n  transform: translateX(-50%);\n  white-space: nowrap;\n}\n\n/* The ends are pulled in so they do not hang outside the plot. */\n.cr-line-chart__label[data-edge='start'] {\n  transform: none;\n}\n\n.cr-line-chart__label[data-edge='end'] {\n  transform: translateX(-100%);\n}\n",
      "type": "registry:file",
      "target": "@components/creance/line-chart/LineChart.css"
    },
    {
      "path": "line-chart/LineChart.tsx",
      "content": "'use client';\n\nimport { useId, useState, type KeyboardEvent, type PointerEvent } from 'react';\nimport { ChartLegend, ChartTable, formatter, niceScale, seriesColor, type ValueFormat } from '@/lib/creance/chart';\nimport { cx } from '@/lib/creance/cx';\nimport { LiveRegion } from '@/lib/creance/LiveRegion';\nimport './LineChart.css';\n\nexport interface LineSeries {\n  name: string;\n  /** One value per label. */\n  values: number[];\n  /** `dashed` marks a comparison, so it differs from the main line in more than shade. */\n  style?: 'solid' | 'dashed';\n}\n\nexport interface LineChartProps extends ValueFormat {\n  /** The points along the bottom, in order: dates, hours. */\n  labels: string[];\n  /** The first series is the main one: solid ink with a soft fill. Two or three read well. */\n  series: LineSeries[];\n  /** What the chart shows, for screen readers and as the table's caption: \"Tokens per day\". */\n  label: string;\n  /** What the labels are, as the heading of the table's first column. Default \"Point\". */\n  axisLabel?: string;\n  /** Height of the plot in pixels. Default 240. */\n  height?: number;\n  /** Names the series. Default true when there is more than one. */\n  legend?: boolean;\n  className?: string;\n}\n\n/**\n * A trend over time. Pointing at the plot, or focusing it and pressing the arrow keys, reads out one point\n * at a time. The whole chart is also a table for screen readers.\n */\nexport function LineChart({ labels, series, label, axisLabel = 'Point', height = 240, legend = series.length > 1, className, ...format }: LineChartProps) {\n  const fmt = formatter(format);\n  const id = useId();\n  const [active, setActive] = useState<number | null>(null);\n  const [spoken, setSpoken] = useState('');\n  const count = labels.length;\n  const scale = niceScale(Math.max(...series.flatMap((s) => s.values), 0));\n  const x = (i: number) => (count > 1 ? (i / (count - 1)) * 100 : 50);\n  const y = (v: number) => 100 - (v / scale.max) * 100;\n  const path = (values: number[]) => values.map((v, i) => `${i ? 'L' : 'M'}${x(i).toFixed(2)} ${y(v).toFixed(2)}`).join(' ');\n  const colors = series.map((_, i) => seriesColor(i === 0 ? 0 : i));\n  // At most seven labels along the bottom, evenly spread.\n  const every = Math.max(1, Math.ceil(count / 7));\n  const describe = (i: number) => `${labels[i]}: ${series.map((s) => `${s.name} ${fmt(s.values[i] ?? 0)}`).join(', ')}`;\n\n  function onPointerMove(e: PointerEvent<HTMLDivElement>) {\n    const box = e.currentTarget.getBoundingClientRect();\n    if (!box.width || count === 0) return;\n    setActive(Math.min(count - 1, Math.max(0, Math.round(((e.clientX - box.left) / box.width) * (count - 1)))));\n  }\n\n  function onKeyDown(e: KeyboardEvent) {\n    const move: Record<string, number> = { ArrowRight: (active ?? -1) + 1, ArrowLeft: (active ?? count) - 1, Home: 0, End: count - 1 };\n    if (e.key === 'Escape') return setActive(null);\n    if (!(e.key in move) || count === 0) return;\n    e.preventDefault();\n    const next = Math.min(count - 1, Math.max(0, move[e.key]!));\n    setActive(next);\n    setSpoken(describe(next));\n  }\n\n  return (\n    <figure className={cx('cr-chart cr-line-chart', className)}>\n      {legend && <ChartLegend items={series.map((s, i) => ({ name: s.name, color: colors[i]!, dashed: s.style === 'dashed' }))} />}\n      {/* Focusable so the points can be read with the arrow keys; the drawing inside is decoration. */}\n      {/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */}\n      <div\n        className=\"cr-chart__plot cr-line-chart__plot\"\n        style={{ height }}\n        role=\"img\"\n        aria-label={`${label}. Press the left and right arrow keys to read each point.`}\n        // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex\n        tabIndex={0}\n        onPointerMove={onPointerMove}\n        onPointerLeave={() => setActive(null)}\n        onKeyDown={onKeyDown}\n        onBlur={() => setActive(null)}\n      >\n        {scale.ticks.map((t) => (\n          <span key={t} className=\"cr-chart__tick\" style={{ bottom: `${(t / scale.max) * 100}%` }}>\n            <span className=\"cr-chart__tick-label\">{fmt(t)}</span>\n          </span>\n        ))}\n        {/* Stretched to the plot; strokes keep their width because they do not scale. */}\n        <svg className=\"cr-line-chart__svg\" viewBox=\"0 0 100 100\" preserveAspectRatio=\"none\" style={{ color: colors[0] }}>\n          <defs>\n            <linearGradient id={id} x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n              <stop offset=\"0\" stopColor=\"currentColor\" stopOpacity=\"0.16\" />\n              <stop offset=\"1\" stopColor=\"currentColor\" stopOpacity=\"0\" />\n            </linearGradient>\n          </defs>\n          {series[0] && count > 1 && <path d={`${path(series[0].values)} L100 100 L0 100 Z`} fill={`url(#${id})`} />}\n          {series\n            .map((s, i) => <path key={i} className=\"cr-line-chart__line\" data-style={s.style ?? 'solid'} d={path(s.values)} stroke={colors[i]} />)\n            // The main series is drawn last, on top.\n            .reverse()}\n        </svg>\n        {active !== null && (\n          <>\n            <span className=\"cr-line-chart__rule\" style={{ left: `${x(active)}%` }} />\n            {series.map((s, i) => (\n              <span key={i} className=\"cr-line-chart__marker\" data-main={i === 0 || undefined} style={{ left: `${x(active)}%`, top: `${y(s.values[active] ?? 0)}%`, color: colors[i] }} />\n            ))}\n            <div className=\"cr-line-chart__tip cr-popover\" data-side={x(active) > 60 ? 'left' : 'right'} style={{ left: `${x(active)}%` }}>\n              <span className=\"cr-line-chart__tip-title\">{labels[active]}</span>\n              <ChartLegend items={series.map((s, i) => ({ name: s.name, color: colors[i]!, dashed: s.style === 'dashed', value: fmt(s.values[active] ?? 0) }))} />\n            </div>\n          </>\n        )}\n      </div>\n      <div className=\"cr-chart__labels\" aria-hidden=\"true\">\n        {labels.map((name, i) =>\n          i % every === 0 ? (\n            <span key={i} className=\"cr-line-chart__label\" data-edge={i === 0 ? 'start' : i === count - 1 ? 'end' : undefined} style={{ left: `${x(i)}%` }}>\n              {name}\n            </span>\n          ) : null,\n        )}\n      </div>\n      <LiveRegion message={spoken} />\n      <ChartTable caption={label} columns={[axisLabel, ...series.map((s) => s.name)]} rows={labels.map((name, i) => [name, ...series.map((s) => fmt(s.values[i] ?? 0))])} />\n    </figure>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@components/creance/line-chart/LineChart.tsx"
    }
  ],
  "categories": [
    "molecule",
    "charts"
  ],
  "type": "registry:ui"
}