{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "chart",
  "title": "Chart",
  "description": "Bar, line, area and donut charts from one data shape, hand-drawn in SVG with nice-number axes, keyboard points and a real data table for assistive tech.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils",
    "https://miseui.junsu4621.workers.dev/r/motion.json"
  ],
  "files": [
    {
      "path": "registry/miseui/ui/chart.tsx",
      "content": "import type * as React from \"react\"\r\nimport { useId, useRef } from \"react\"\r\nimport { useReducedMotion } from \"motion/react\"\r\nimport { CartesianAxes } from \"@/registry/miseui/ui/chart-axes\"\r\nimport { BarMarks, DonutMarks, LineMarks } from \"@/registry/miseui/ui/chart-marks\"\r\nimport { chartModel, type ChartDatum, type ChartSeries, type ChartVariant } from \"@/registry/miseui/ui/chart-model\"\r\nimport { ChartDataTable, ChartLegend, ChartTooltip, DonutLegend } from \"@/registry/miseui/ui/chart-parts\"\r\nimport { useActivePoint, useElementWidth, useSeenOnce } from \"@/registry/miseui/ui/chart-state\"\r\nimport { cn } from \"@/registry/miseui/lib/utils\"\r\n\r\ntype ChartEffect = \"draw\"\r\n\r\nconst DEFAULT_HEIGHT = 200\r\nconst FALLBACK_WIDTH = 480\r\nconst compact = new Intl.NumberFormat(undefined, { notation: \"compact\", maximumFractionDigits: 1 })\r\nconst defaultFormat = (value: number) => compact.format(value)\r\n\r\ninterface ChartProps extends Omit<React.ComponentProps<\"div\">, \"children\"> {\r\n  /** Names the chart for assistive tech and captions its data table. */\r\n  label: string\r\n  data: readonly ChartDatum[]\r\n  /** The field of each datum that names it, such as \"month\". */\r\n  index: string\r\n  /** Every variant reads the same shape. `donut` uses the first series and gives each datum a slice. */\r\n  series: readonly ChartSeries[]\r\n  /** `bar` compares amounts, `line` shows a trend, `area` a trend of a total, `donut` shares of a whole. */\r\n  variant?: ChartVariant\r\n  /** `draw` builds the marks once, the first time the chart scrolls into view. */\r\n  effect?: ChartEffect\r\n  height?: number\r\n  formatValue?: (value: number) => string\r\n  /** Shown instead of the plot when there is nothing to draw. */\r\n  emptyText?: string\r\n  /** Read to assistive tech when the plot takes focus. */\r\n  hint?: string\r\n}\r\n\r\ntype Model = ReturnType<typeof chartModel>\r\n\r\ninterface ChartMarksProps {\r\n  model: Model\r\n  variant: ChartVariant\r\n  active: number | null\r\n  draw: { drawn: boolean; animated: boolean }\r\n  format: (value: number) => string\r\n}\r\n\r\nfunction ChartMarks({ model, variant, active, draw, format }: ChartMarksProps) {\r\n  if (model.donut) return <DonutMarks slices={model.slices} {...model.geometry} active={active} {...draw} />\r\n  const shared = { layout: model.layout, series: model.values, active, ...draw }\r\n  return (\r\n    <>\r\n      <CartesianAxes layout={model.layout} labels={model.labels} format={format} />\r\n      {variant === \"bar\" ? <BarMarks {...shared} /> : <LineMarks {...shared} filled={variant === \"area\"} />}\r\n    </>\r\n  )\r\n}\r\n\r\nfunction DonutCentre({ total, label }: { total: string; label?: string }) {\r\n  return (\r\n    <div aria-hidden=\"true\" className=\"pointer-events-none absolute inset-0 grid place-content-center text-center\">\r\n      <p data-slot=\"chart-total\" className=\"text-xl font-semibold tabular-nums\">\r\n        {total}\r\n      </p>\r\n      <p className=\"text-[13px] text-muted-foreground\">{label}</p>\r\n    </div>\r\n  )\r\n}\r\n\r\nfunction useDraw(plot: React.RefObject<HTMLElement | null>, effect: ChartEffect | undefined) {\r\n  const reduceMotion = useReducedMotion() ?? false\r\n  const animated = effect === \"draw\" && !reduceMotion\r\n  const seen = useSeenOnce(plot, animated)\r\n  return { drawn: !animated || seen, animated }\r\n}\r\n\r\ninterface ChartBodyProps {\r\n  plot: React.RefObject<HTMLDivElement | null>\r\n  model: Model\r\n  variant: ChartVariant\r\n  series: readonly ChartSeries[]\r\n  width: number\r\n  height: number\r\n  draw: ChartMarksProps[\"draw\"]\r\n  format: (value: number) => string\r\n  active: number | null\r\n  surface: ReturnType<typeof useActivePoint>[\"surface\"]\r\n  label: string\r\n  hintId: string\r\n  emptyText: string\r\n}\r\n\r\nfunction ChartPlot({ plot, model, variant, series, width, height, draw, format, active, surface, label, hintId, emptyText }: ChartBodyProps) {\r\n  const empty = model.count === 0\r\n  const tip = active === null ? null : model.tipAt(active)\r\n  return (\r\n    <div\r\n      ref={plot}\r\n      data-slot=\"chart-plot\"\r\n      role=\"group\"\r\n      aria-roledescription=\"chart\"\r\n      aria-label={label}\r\n      aria-describedby={hintId}\r\n      tabIndex={empty ? undefined : 0}\r\n      className=\"relative w-full min-w-0 touch-pan-y rounded-md outline-none select-none focus-visible:ring-[3px] focus-visible:ring-ring/50\"\r\n      style={{ height }}\r\n      {...(empty ? {} : surface)}\r\n    >\r\n      {empty ? (\r\n        <p className=\"grid h-full place-items-center text-[13px] text-muted-foreground\">{emptyText}</p>\r\n      ) : (\r\n        <svg aria-hidden=\"true\" width=\"100%\" height={height} viewBox={`0 0 ${width} ${height}`} className=\"block\">\r\n          <ChartMarks model={model} variant={variant} active={active} draw={draw} format={format} />\r\n        </svg>\r\n      )}\r\n      {model.donut && !empty ? <DonutCentre total={format(model.total)} label={series[0]?.label} /> : null}\r\n      {tip && active !== null ? (\r\n        <ChartTooltip title={model.titleAt(active)} rows={model.rowsAt(active)} x={tip.x} y={tip.y} width={width} line={variant === \"line\" || variant === \"area\"} />\r\n      ) : null}\r\n    </div>\r\n  )\r\n}\r\n\r\nfunction Chart({\r\n  label,\r\n  data,\r\n  index,\r\n  series,\r\n  variant = \"bar\",\r\n  effect,\r\n  height = DEFAULT_HEIGHT,\r\n  formatValue = defaultFormat,\r\n  emptyText = \"No data yet\",\r\n  hint = \"Use the arrow keys to move between points.\",\r\n  className,\r\n  ...props\r\n}: ChartProps) {\r\n  const plot = useRef<HTMLDivElement>(null)\r\n  const hintId = useId()\r\n  const width = useElementWidth(plot, FALLBACK_WIDTH)\r\n  const draw = useDraw(plot, effect)\r\n  const model = chartModel({ data, index, series, variant, width, height, format: formatValue })\r\n  const { active, viaKeyboard, surface } = useActivePoint(model.count, model.hit)\r\n  const empty = model.count === 0\r\n  const spoken = viaKeyboard && active !== null ? `${model.titleAt(active)}: ${model.rowsAt(active).map((row) => `${row.label} ${row.value}`).join(\", \")}` : \"\"\r\n  const body = { plot, model, variant, series, width, height, draw, format: formatValue, active, surface, label, hintId, emptyText }\r\n  return (\r\n    <div data-slot=\"chart\" data-variant={variant} data-effect={effect} className={cn(\"grid w-full min-w-0 grid-cols-[minmax(0,1fr)] gap-3\", className)} {...props}>\r\n      {!model.donut && series.length > 1 && !empty ? <ChartLegend labels={series.map((entry) => entry.label)} line={variant !== \"bar\"} /> : null}\r\n      <ChartPlot {...body} />\r\n      {model.donut && !empty ? (\r\n        <DonutLegend rows={model.slices.map((slice) => ({ label: model.labels[slice.index], value: formatValue(slice.value) }))} active={active} />\r\n      ) : null}\r\n      <span id={hintId} className=\"sr-only\">\r\n        {hint}\r\n      </span>\r\n      <span data-slot=\"chart-live\" aria-live=\"polite\" className=\"sr-only\">\r\n        {spoken}\r\n      </span>\r\n      <ChartDataTable caption={label} indexLabel={index} columns={series.map((entry) => entry.label)} rows={model.tableRows} />\r\n    </div>\r\n  )\r\n}\r\n\r\nexport { Chart, type ChartProps, type ChartVariant, type ChartEffect, type ChartDatum, type ChartSeries }\r\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/chart-axes.tsx",
      "content": "import type { CartesianLayout } from \"@/registry/miseui/ui/chart-state\"\r\n\r\nfunction CartesianAxes({ layout, labels, format }: { layout: CartesianLayout; labels: readonly string[]; format: (value: number) => string }) {\r\n  const anchorOf = (index: number) => {\r\n    if (layout.band) return \"middle\"\r\n    if (index === 0) return \"start\"\r\n    return index === labels.length - 1 ? \"end\" : \"middle\"\r\n  }\r\n  return (\r\n    <g className=\"fill-muted-foreground text-xs tabular-nums\">\r\n      {layout.ticks.map((tick) => (\r\n        <g key={tick}>\r\n          <line x1={layout.left} x2={layout.right} y1={layout.y(tick)} y2={layout.y(tick)} className=\"stroke-border\" strokeDasharray={tick === 0 ? undefined : \"2 4\"} />\r\n          <text x={layout.left - 8} y={layout.y(tick)} dy=\"0.32em\" textAnchor=\"end\">\r\n            {format(tick)}\r\n          </text>\r\n        </g>\r\n      ))}\r\n      {labels.map((label, index) =>\r\n        index % layout.stride === 0 ? (\r\n          <text key={index} data-slot=\"chart-tick\" x={layout.positions[index]} y={layout.bottom + 18} textAnchor={anchorOf(index)}>\r\n            {label}\r\n          </text>\r\n        ) : null,\r\n      )}\r\n    </g>\r\n  )\r\n}\r\n\r\nexport { CartesianAxes }\r\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/chart-marks.tsx",
      "content": "import { motion, type Transition } from \"motion/react\"\r\nimport { SPRING_DEFAULT } from \"@/registry/miseui/lib/motion\"\r\nimport { arcPath, areaPath, barPath, linePath, type DonutSlice } from \"@/registry/miseui/ui/chart-math\"\r\nimport type { CartesianLayout } from \"@/registry/miseui/ui/chart-state\"\r\n\r\nconst BAR_MAX_WIDTH = 32\r\nconst BAR_GAP = 2\r\nconst BAR_RADIUS = 4\r\nconst BAR_GROUP_SHARE = 0.68\r\nconst LINE_WIDTH = 2\r\nconst POINT_RADIUS = 4\r\nconst STAGGER_SECONDS = 0.03\r\nconst MAX_DELAY_SECONDS = 0.3\r\nconst DRAW_LINE: Transition = { ...SPRING_DEFAULT, duration: 0.9 }\r\nconst DRAW_BAR: Transition = { ...SPRING_DEFAULT, duration: 0.6 }\r\nconst MIX = (percent: number) => `color-mix(in oklab, var(--muted-foreground) ${percent}%, var(--background))`\r\nconst GREYS = [MIX(100), MIX(62), MIX(38)]\r\nconst DASHES = [undefined, undefined, \"6 4\", \"2 4\"]\r\n\r\n/** The accent carries the first series; everything else is grey, so a chart never turns into a rainbow. */\r\nconst seriesColor = (index: number) => (index === 0 ? \"var(--mise)\" : GREYS[(index - 1) % GREYS.length])\r\n/** Greys can look alike, so later lines also differ in dash pattern. */\r\nconst seriesDash = (index: number) => DASHES[index % DASHES.length]\r\n\r\ninterface DrawState {\r\n  drawn: boolean\r\n  animated: boolean\r\n}\r\n\r\ninterface CartesianMarksProps extends DrawState {\r\n  layout: CartesianLayout\r\n  series: readonly (readonly (number | null)[])[]\r\n  active: number | null\r\n}\r\n\r\nfunction ActiveBand({ layout, active }: Pick<CartesianMarksProps, \"layout\" | \"active\">) {\r\n  if (active === null) return null\r\n  return (\r\n    <rect\r\n      data-slot=\"chart-active\"\r\n      x={layout.positions[active] - layout.bandWidth / 2}\r\n      y={layout.top}\r\n      width={layout.bandWidth}\r\n      height={layout.bottom - layout.top}\r\n      rx={BAR_RADIUS}\r\n      className=\"fill-foreground/6\"\r\n    />\r\n  )\r\n}\r\n\r\nfunction BarMarks({ layout, series, active, drawn, animated }: CartesianMarksProps) {\r\n  const groupWidth = Math.min(layout.bandWidth * BAR_GROUP_SHARE, BAR_MAX_WIDTH * series.length)\r\n  const barWidth = Math.max(2, (groupWidth - BAR_GAP * (series.length - 1)) / series.length)\r\n  return (\r\n    <g>\r\n      <ActiveBand layout={layout} active={active} />\r\n      {series.map((values, seriesIndex) =>\r\n        values.map((value, index) => {\r\n          if (value === null) return null\r\n          const left = layout.positions[index] - groupWidth / 2 + seriesIndex * (barWidth + BAR_GAP)\r\n          const d = barPath(left, layout.y(value), barWidth, layout.baseline, BAR_RADIUS)\r\n          if (!d) return null\r\n          const key = `${seriesIndex}-${index}`\r\n          const attrs = { \"data-slot\": \"chart-bar\", d, fill: seriesColor(seriesIndex) }\r\n          if (!animated) return <path key={key} {...attrs} />\r\n          return (\r\n            <motion.path\r\n              key={key}\r\n              {...attrs}\r\n              style={{ originY: value < 0 ? 0 : 1 }}\r\n              initial={{ scaleY: 0 }}\r\n              animate={{ scaleY: drawn ? 1 : 0 }}\r\n              transition={{ ...DRAW_BAR, delay: Math.min(MAX_DELAY_SECONDS, index * STAGGER_SECONDS) }}\r\n            />\r\n          )\r\n        }),\r\n      )}\r\n    </g>\r\n  )\r\n}\r\n\r\nfunction SeriesLine({ d, index, drawn, animated }: DrawState & { d: string; index: number }) {\r\n  const dash = seriesDash(index)\r\n  const attrs = {\r\n    \"data-slot\": \"chart-line\",\r\n    d,\r\n    fill: \"none\",\r\n    stroke: seriesColor(index),\r\n    strokeWidth: LINE_WIDTH,\r\n    strokeLinecap: \"round\" as const,\r\n    strokeLinejoin: \"round\" as const,\r\n  }\r\n  if (!animated) return <path {...attrs} strokeDasharray={dash} />\r\n  // pathLength owns the dash array while it draws, so a dashed line fades in instead.\r\n  if (dash) {\r\n    return <motion.path {...attrs} strokeDasharray={dash} initial={{ opacity: 0 }} animate={{ opacity: drawn ? 1 : 0 }} transition={DRAW_LINE} />\r\n  }\r\n  return (\r\n    <motion.path\r\n      {...attrs}\r\n      initial={{ pathLength: 0, opacity: 0 }}\r\n      animate={{ pathLength: drawn ? 1 : 0, opacity: drawn ? 1 : 0 }}\r\n      transition={{ pathLength: DRAW_LINE, opacity: { duration: 0 } }}\r\n    />\r\n  )\r\n}\r\n\r\nfunction SeriesArea({ d, index, drawn, animated }: DrawState & { d: string; index: number }) {\r\n  const attrs = { \"data-slot\": \"chart-area\", d, fill: seriesColor(index), stroke: \"none\" }\r\n  const opacity = index === 0 ? 0.2 : 0.14\r\n  if (!animated) return <path {...attrs} fillOpacity={opacity} />\r\n  return (\r\n    <motion.path {...attrs} initial={{ fillOpacity: 0 }} animate={{ fillOpacity: drawn ? opacity : 0 }} transition={{ ...DRAW_LINE, delay: 0.2 }} />\r\n  )\r\n}\r\n\r\nfunction ActivePoints({ layout, series, active }: Pick<CartesianMarksProps, \"layout\" | \"series\" | \"active\">) {\r\n  if (active === null) return null\r\n  const x = layout.positions[active]\r\n  return (\r\n    <g data-slot=\"chart-active\">\r\n      <line x1={x} x2={x} y1={layout.top} y2={layout.bottom} className=\"stroke-border\" />\r\n      {series.map((values, index) => {\r\n        const value = values[active]\r\n        if (value === null) return null\r\n        return <circle key={index} cx={x} cy={layout.y(value)} r={POINT_RADIUS} fill={seriesColor(index)} className=\"stroke-background\" strokeWidth={2} />\r\n      })}\r\n    </g>\r\n  )\r\n}\r\n\r\nfunction LineMarks({ layout, series, active, drawn, animated, filled }: CartesianMarksProps & { filled: boolean }) {\r\n  const pointsOf = (values: readonly (number | null)[]) =>\r\n    values.map((value, index) => (value === null ? null : { x: layout.positions[index], y: layout.y(value) }))\r\n  return (\r\n    <g>\r\n      {filled\r\n        ? series.map((values, index) => {\r\n            const d = areaPath(pointsOf(values), layout.baseline)\r\n            return d ? <SeriesArea key={index} d={d} index={index} drawn={drawn} animated={animated} /> : null\r\n          })\r\n        : null}\r\n      {series.map((values, index) => (\r\n        <SeriesLine key={index} d={linePath(pointsOf(values))} index={index} drawn={drawn} animated={animated} />\r\n      ))}\r\n      <ActivePoints layout={layout} series={series} active={active} />\r\n    </g>\r\n  )\r\n}\r\n\r\ninterface DonutMarksProps extends DrawState {\r\n  slices: readonly DonutSlice[]\r\n  cx: number\r\n  cy: number\r\n  radius: number\r\n  thickness: number\r\n  active: number | null\r\n}\r\n\r\nfunction DonutMarks({ slices, cx, cy, radius, thickness, active, drawn, animated }: DonutMarksProps) {\r\n  return (\r\n    <g>\r\n      {slices.map((slice, position) => {\r\n        const attrs = {\r\n          \"data-slot\": \"chart-slice\",\r\n          \"data-active\": active === position ? \"true\" : undefined,\r\n          d: arcPath(cx, cy, radius, slice.start, slice.end),\r\n          fill: \"none\",\r\n          stroke: seriesColor(position),\r\n          strokeWidth: active === position ? thickness + 4 : thickness,\r\n        }\r\n        if (!animated) return <path key={slice.index} {...attrs} />\r\n        return (\r\n          <motion.path\r\n            key={slice.index}\r\n            {...attrs}\r\n            initial={{ pathLength: 0, opacity: 0 }}\r\n            animate={{ pathLength: drawn ? 1 : 0, opacity: drawn ? 1 : 0 }}\r\n            transition={{ pathLength: { ...DRAW_LINE, delay: (slice.start / (Math.PI * 2)) * 0.4 }, opacity: { duration: 0 } }}\r\n          />\r\n        )\r\n      })}\r\n    </g>\r\n  )\r\n}\r\n\r\nexport { BarMarks, DonutMarks, LineMarks, seriesColor, seriesDash }\r\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/chart-math.ts",
      "content": "interface ChartTicks {\n  ticks: readonly number[]\n  min: number\n  max: number\n  step: number\n}\n\ninterface ChartPoint {\n  x: number\n  y: number\n}\n\ninterface DonutSlice {\n  index: number\n  value: number\n  share: number\n  /** Radians clockwise from twelve o'clock. */\n  start: number\n  end: number\n}\n\nconst FULL_TURN = Math.PI * 2\nconst round = (value: number) => Math.round(value * 100) / 100\n\n/** Rounds up to 1, 2, 2.5, 5 or 10 times a power of ten, the steps people can add in their head. */\nfunction niceStep(span: number, count: number) {\n  const rough = span / Math.max(1, count)\n  const power = 10 ** Math.floor(Math.log10(rough))\n  const fraction = rough / power\n  const nice = fraction <= 1 ? 1 : fraction <= 2 ? 2 : fraction <= 2.5 ? 2.5 : fraction <= 5 ? 5 : 10\n  return nice * power\n}\n\n/** A flat series still needs an axis with height, or every mark lands on one line at the edge. */\nfunction widenFlat(low: number, high: number) {\n  if (high > low) return [low, high]\n  if (high === 0) return [0, 1]\n  const pad = Math.abs(high) * 0.1\n  return [low - pad, high + pad]\n}\n\nfunction niceTicks(values: readonly number[], count = 4, includeZero = true): ChartTicks {\n  const finite = values.filter((value) => Number.isFinite(value))\n  const seen = finite.length > 0 ? finite : [0]\n  const low = includeZero ? Math.min(0, ...seen) : Math.min(...seen)\n  const high = includeZero ? Math.max(0, ...seen) : Math.max(...seen)\n  const [from, to] = widenFlat(low, high)\n  const step = niceStep(to - from, count)\n  const min = Math.floor(from / step) * step\n  const max = Math.ceil(to / step) * step\n  const steps = Math.round((max - min) / step)\n  // Multiplying instead of adding keeps 0.1 + 0.2 out of the tick labels.\n  const decimals = Math.max(0, 1 - Math.floor(Math.log10(step)))\n  const ticks = Array.from({ length: steps + 1 }, (_, index) => Number((min + index * step).toFixed(decimals)))\n  return { ticks, min: ticks[0], max: ticks[ticks.length - 1], step }\n}\n\nfunction scaleLinear(domain: readonly [number, number], range: readonly [number, number]) {\n  const [d0, d1] = domain\n  const [r0, r1] = range\n  const ratio = d1 === d0 ? 0 : (r1 - r0) / (d1 - d0)\n  return (value: number) => r0 + (value - d0) * ratio\n}\n\n/** Line and area run edge to edge, so the first and last points sit on the plot's sides. */\nfunction pointPositions(count: number, width: number) {\n  if (count <= 1) return count === 1 ? [width / 2] : []\n  return Array.from({ length: count }, (_, index) => (index * width) / (count - 1))\n}\n\nfunction bandPositions(count: number, width: number) {\n  const band = count > 0 ? width / count : 0\n  return Array.from({ length: count }, (_, index) => (index + 0.5) * band)\n}\n\nfunction nearestIndex(positions: readonly number[], x: number) {\n  if (positions.length === 0) return null\n  return positions.reduce((best, position, index) => (Math.abs(position - x) < Math.abs(positions[best] - x) ? index : best), 0)\n}\n\nconst NARROW_CHAR_PX = 8\nconst WIDE_CHAR_PX = 13\n\n/** Axis labels at 12 px. Compact numbers come back as 1.5K or 1.5천 depending on locale, and a wide character takes far more room. */\nfunction axisLabelWidth(label: string) {\n  return Array.from(label).reduce((total, char) => total + ((char.codePointAt(0) ?? 0) > 0x2e7f ? WIDE_CHAR_PX : NARROW_CHAR_PX), 0)\n}\n\n/** How many labels to skip so the longest one still fits. Always a whole stride, so the rhythm stays even. */\nfunction labelStride(count: number, width: number, longestChars: number, charWidth = 7) {\n  if (count <= 1 || width <= 0) return 1\n  const needed = longestChars * charWidth + 12\n  return Math.max(1, Math.ceil((needed * count) / width))\n}\n\nfunction splitSegments(points: readonly (ChartPoint | null)[]) {\n  return points.reduce<ChartPoint[][]>((segments, point) => {\n    if (point === null) return segments.at(-1)?.length === 0 ? segments : [...segments, []]\n    const last = segments.at(-1) ?? []\n    return [...segments.slice(0, -1), [...last, point]]\n  }, [[]]).filter((segment) => segment.length > 0)\n}\n\n/** A missing value breaks the line. Joining across it would draw a figure nobody measured. */\nfunction linePath(points: readonly (ChartPoint | null)[]) {\n  return splitSegments(points)\n    .map((segment) => {\n      const [first, ...rest] = segment\n      const start = `M${round(first.x)} ${round(first.y)}`\n      // A lone point has no length to stroke; h0 with a round cap paints it as a dot.\n      if (rest.length === 0) return `${start}h0`\n      return `${start}${rest.map((point) => `L${round(point.x)} ${round(point.y)}`).join(\"\")}`\n    })\n    .join(\"\")\n}\n\nfunction areaPath(points: readonly (ChartPoint | null)[], baseline: number) {\n  return splitSegments(points)\n    .filter((segment) => segment.length > 1)\n    .map((segment) => {\n      const line = segment.map((point) => `L${round(point.x)} ${round(point.y)}`).join(\"\")\n      const first = segment[0]\n      const last = segment[segment.length - 1]\n      return `M${round(first.x)} ${round(baseline)}${line}L${round(last.x)} ${round(baseline)}Z`\n    })\n    .join(\"\")\n}\n\n/** Rounds only the end away from the baseline, so a negative bar hangs from zero the same way. */\nfunction barPath(left: number, top: number, width: number, baseline: number, radius: number) {\n  const height = Math.abs(baseline - top)\n  if (height === 0 || width <= 0) return \"\"\n  const r = Math.min(radius, width / 2, height)\n  const dir = top < baseline ? 1 : -1\n  const right = left + width\n  return [\n    `M${round(left)} ${round(baseline)}`,\n    `L${round(left)} ${round(top + dir * r)}`,\n    `Q${round(left)} ${round(top)} ${round(left + r)} ${round(top)}`,\n    `L${round(right - r)} ${round(top)}`,\n    `Q${round(right)} ${round(top)} ${round(right)} ${round(top + dir * r)}`,\n    `L${round(right)} ${round(baseline)}Z`,\n  ].join(\"\")\n}\n\nfunction polar(cx: number, cy: number, radius: number, angle: number) {\n  return { x: cx + radius * Math.sin(angle), y: cy - radius * Math.cos(angle) }\n}\n\n/** An SVG arc whose ends meet draws nothing, so a whole circle goes in two halves. */\nfunction arcPath(cx: number, cy: number, radius: number, start: number, end: number): string {\n  const sweep = end - start\n  if (sweep >= FULL_TURN - 0.0001) {\n    const middle = start + sweep / 2\n    return `${arcPath(cx, cy, radius, start, middle)}${arcPath(cx, cy, radius, middle, end).replace(/^M[^A]+/, \"\")}`\n  }\n  const from = polar(cx, cy, radius, start)\n  const to = polar(cx, cy, radius, end)\n  const large = sweep > Math.PI ? 1 : 0\n  return `M${round(from.x)} ${round(from.y)}A${round(radius)} ${round(radius)} 0 ${large} 1 ${round(to.x)} ${round(to.y)}`\n}\n\n/** Zero and negative values have no share of a whole, so they get no slice. */\nfunction donutSlices(values: readonly (number | null)[], gap = 0.03): readonly DonutSlice[] {\n  const parts = values.map((value, index) => ({ index, value: value ?? 0 })).filter((part) => part.value > 0)\n  const total = parts.reduce((sum, part) => sum + part.value, 0)\n  if (total === 0) return []\n  const inset = parts.length > 1 ? gap / 2 : 0\n  return parts.reduce<DonutSlice[]>((slices, part) => {\n    const from = slices.length > 0 ? slices[slices.length - 1].end + inset : 0\n    const share = part.value / total\n    const start = from + inset\n    return [...slices, { index: part.index, value: part.value, share, start, end: from + share * FULL_TURN - inset }]\n  }, [])\n}\n\nfunction angleAt(x: number, y: number, cx: number, cy: number) {\n  const angle = Math.atan2(x - cx, cy - y)\n  return angle < 0 ? angle + FULL_TURN : angle\n}\n\n/** The gaps between slices belong to the slice before them, so the pointer never falls into a hole. */\nfunction sliceAtAngle(slices: readonly DonutSlice[], angle: number) {\n  const hit = slices.find((slice) => angle <= slice.end)\n  return (hit ?? slices[0])?.index ?? null\n}\n\nexport {\n  FULL_TURN,\n  angleAt,\n  arcPath,\n  areaPath,\n  axisLabelWidth,\n  bandPositions,\n  barPath,\n  donutSlices,\n  labelStride,\n  linePath,\n  nearestIndex,\n  niceStep,\n  niceTicks,\n  pointPositions,\n  polar,\n  scaleLinear,\n  sliceAtAngle,\n  type ChartPoint,\n  type ChartTicks,\n  type DonutSlice,\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/chart-model.ts",
      "content": "import { angleAt, donutSlices, nearestIndex, polar, sliceAtAngle } from \"@/registry/miseui/ui/chart-math\"\r\nimport { cartesianLayout } from \"@/registry/miseui/ui/chart-state\"\r\n\r\ntype ChartVariant = \"bar\" | \"line\" | \"area\" | \"donut\"\r\ntype ChartDatum = Record<string, string | number | null | undefined>\r\n\r\ninterface ChartSeries {\r\n  /** The field of each datum that holds this series. */\r\n  key: string\r\n  label: string\r\n}\r\n\r\ninterface ChartModelInput {\r\n  data: readonly ChartDatum[]\r\n  index: string\r\n  series: readonly ChartSeries[]\r\n  variant: ChartVariant\r\n  width: number\r\n  height: number\r\n  format: (value: number) => string\r\n}\r\n\r\nconst DONUT_PADDING = 8\r\nconst DONUT_THICKNESS_SHARE = 0.13\r\nconst MIN_DONUT_THICKNESS = 12\r\nconst percent = new Intl.NumberFormat(undefined, { style: \"percent\", maximumFractionDigits: 1 })\r\n\r\nconst readNumber = (datum: ChartDatum, key: string) => {\r\n  const value = datum[key]\r\n  return typeof value === \"number\" && Number.isFinite(value) ? value : null\r\n}\r\n\r\nfunction donutGeometry(width: number, height: number) {\r\n  const size = Math.min(width, height)\r\n  const thickness = Math.max(MIN_DONUT_THICKNESS, Math.round(size * DONUT_THICKNESS_SHARE))\r\n  return { cx: width / 2, cy: height / 2, radius: Math.max(1, size / 2 - thickness / 2 - DONUT_PADDING), thickness }\r\n}\r\n\r\nfunction tableRowsOf(labels: readonly string[], values: readonly (readonly (number | null)[])[], format: (value: number) => string) {\r\n  return labels.map((label, rowIndex) => ({\r\n    label,\r\n    values: values.map((column) => {\r\n      const value = column[rowIndex]\r\n      return value === null ? \"No data\" : format(value)\r\n    }),\r\n  }))\r\n}\r\n\r\n/** Every position below is a place in keyboard order: on a donut, zero values have no slice to stop on. */\r\nfunction chartModel({ data, index, series, variant, width, height, format }: ChartModelInput) {\r\n  const donut = variant === \"donut\"\r\n  const labels = data.map((datum) => String(datum[index] ?? \"\"))\r\n  const values = series.map((entry) => data.map((datum) => readNumber(datum, entry.key)))\r\n  const flat = values.flat().filter((value): value is number => value !== null)\r\n  const slices = donut ? donutSlices(values[0] ?? []) : []\r\n  const geometry = donutGeometry(width, height)\r\n  const layout = cartesianLayout({ width, height, labels, values: flat, band: variant === \"bar\", includeZero: variant !== \"line\", format })\r\n\r\n  const hit = (x: number, y: number) => {\r\n    if (!donut) return nearestIndex(layout.positions, x)\r\n    const distance = Math.hypot(x - geometry.cx, y - geometry.cy)\r\n    if (slices.length === 0 || Math.abs(distance - geometry.radius) > geometry.thickness / 2 + 4) return null\r\n    const dataIndex = sliceAtAngle(slices, angleAt(x, y, geometry.cx, geometry.cy))\r\n    return slices.findIndex((slice) => slice.index === dataIndex)\r\n  }\r\n  const titleAt = (position: number) => labels[donut ? slices[position].index : position]\r\n  const rowsAt = (position: number) => {\r\n    if (donut) {\r\n      const slice = slices[position]\r\n      return [{ key: \"value\", label: percent.format(slice.share), value: format(slice.value), colorIndex: position }]\r\n    }\r\n    return series.map((entry, seriesIndex) => {\r\n      const value = values[seriesIndex][position]\r\n      return { key: entry.key, label: entry.label, value: value === null ? \"No data\" : format(value), colorIndex: seriesIndex }\r\n    })\r\n  }\r\n  const tipAt = (position: number) => {\r\n    if (!donut) return { x: layout.positions[position], y: layout.top }\r\n    const slice = slices[position]\r\n    return polar(geometry.cx, geometry.cy, geometry.radius, (slice.start + slice.end) / 2)\r\n  }\r\n  return {\r\n    donut,\r\n    labels,\r\n    values,\r\n    slices,\r\n    geometry,\r\n    layout,\r\n    total: flat.reduce((sum, value) => sum + value, 0),\r\n    count: donut ? slices.length : labels.length,\r\n    hit,\r\n    titleAt,\r\n    rowsAt,\r\n    tipAt,\r\n    tableRows: tableRowsOf(labels, values, format),\r\n  }\r\n}\r\n\r\nexport { chartModel, type ChartDatum, type ChartSeries, type ChartVariant }\r\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/chart-parts.tsx",
      "content": "import { seriesColor, seriesDash } from \"@/registry/miseui/ui/chart-marks\"\nimport { cn } from \"@/registry/miseui/lib/utils\"\n\nconst TIP_GAP = 12\nconst TIP_EDGE = 4\nconst TIP_MAX_WIDTH = 224\n\ninterface ChartKeyProps {\n  index: number\n  line: boolean\n}\n\n/** A line series keys with a stroke, so the dash pattern that tells greys apart shows in the legend too. */\nfunction ChartKey({ index, line }: ChartKeyProps) {\n  if (line) {\n    return (\n      <svg aria-hidden=\"true\" width=\"16\" height=\"4\" className=\"shrink-0\">\n        <line x1=\"1\" x2=\"15\" y1=\"2\" y2=\"2\" stroke={seriesColor(index)} strokeWidth=\"2\" strokeLinecap=\"round\" strokeDasharray={seriesDash(index)} />\n      </svg>\n    )\n  }\n  return <span aria-hidden=\"true\" className=\"size-2 shrink-0 rounded-[2px]\" style={{ backgroundColor: seriesColor(index) }} />\n}\n\ninterface TooltipRow {\n  key: string\n  label: string\n  value: string\n  colorIndex: number\n}\n\ninterface ChartTooltipProps {\n  title: string\n  rows: readonly TooltipRow[]\n  x: number\n  y: number\n  /** Tips open away from the nearest side, so they stay inside the chart and off the point itself. */\n  width: number\n  line: boolean\n}\n\n/** Past the edge of a narrow chart by a few pixels is better than a series name cut down to three letters. */\nconst TIP_MIN_WIDTH = 152\n\nfunction ChartTooltip({ title, rows, x, y, width, line }: ChartTooltipProps) {\n  const opensRight = x < width / 2\n  const room = (opensRight ? width - x : x) - TIP_GAP - TIP_EDGE\n  return (\n    <div\n      data-slot=\"chart-tooltip\"\n      data-side={opensRight ? \"right\" : \"left\"}\n      aria-hidden=\"true\"\n      className=\"pointer-events-none absolute z-10 w-max rounded-md border bg-popover px-3 py-2 text-[13px] text-popover-foreground shadow-md\"\n      style={{ left: x, top: y, maxWidth: Math.min(TIP_MAX_WIDTH, Math.max(TIP_MIN_WIDTH, room)), transform: opensRight ? `translateX(${TIP_GAP}px)` : `translateX(calc(-100% - ${TIP_GAP}px))` }}\n    >\n      <p className=\"truncate text-muted-foreground\">{title}</p>\n      <ul className=\"mt-1 grid grid-cols-[minmax(0,1fr)] gap-1\">\n        {rows.map((row) => (\n          <li key={row.key} className=\"flex items-center gap-2\">\n            <ChartKey index={row.colorIndex} line={line} />\n            <span className=\"min-w-0 truncate\">{row.label}</span>\n            <span className=\"ms-auto ps-3 font-medium tabular-nums\">{row.value}</span>\n          </li>\n        ))}\n      </ul>\n    </div>\n  )\n}\n\nfunction ChartLegend({ labels, line }: { labels: readonly string[]; line: boolean }) {\n  return (\n    <ul data-slot=\"chart-legend\" className=\"flex flex-wrap gap-x-4 gap-y-1 text-[13px] text-muted-foreground\">\n      {labels.map((label, index) => (\n        <li key={label} className=\"flex min-w-0 items-center gap-2\">\n          <ChartKey index={index} line={line} />\n          <span className=\"truncate\">{label}</span>\n        </li>\n      ))}\n    </ul>\n  )\n}\n\ninterface DonutLegendRow {\n  label: string\n  value: string\n}\n\nfunction DonutLegend({ rows, active }: { rows: readonly DonutLegendRow[]; active: number | null }) {\n  return (\n    <ul data-slot=\"chart-legend\" className=\"grid grid-cols-[minmax(0,1fr)] gap-1 text-[13px]\">\n      {rows.map((row, index) => (\n        <li key={row.label} className={cn(\"flex min-w-0 items-center gap-2 text-muted-foreground\", active === index && \"text-foreground\")}>\n          <ChartKey index={index} line={false} />\n          <span className=\"min-w-0 truncate\">{row.label}</span>\n          <span className=\"ms-auto ps-3 tabular-nums\">{row.value}</span>\n        </li>\n      ))}\n    </ul>\n  )\n}\n\ninterface ChartDataTableProps {\n  caption: string\n  indexLabel: string\n  columns: readonly string[]\n  rows: readonly { label: string; values: readonly string[] }[]\n}\n\n/** The SVG is hidden from assistive tech, so this is the chart for anyone who cannot see it. */\nfunction ChartDataTable({ caption, indexLabel, columns, rows }: ChartDataTableProps) {\n  return (\n    <table data-slot=\"chart-table\" className=\"sr-only\">\n      <caption>{caption}</caption>\n      <thead>\n        <tr>\n          <th scope=\"col\">{indexLabel}</th>\n          {columns.map((column) => (\n            <th key={column} scope=\"col\">\n              {column}\n            </th>\n          ))}\n        </tr>\n      </thead>\n      <tbody>\n        {rows.map((row, rowIndex) => (\n          <tr key={rowIndex}>\n            <th scope=\"row\">{row.label}</th>\n            {row.values.map((value, index) => (\n              <td key={index}>{value}</td>\n            ))}\n          </tr>\n        ))}\n      </tbody>\n    </table>\n  )\n}\n\nexport { ChartDataTable, ChartLegend, ChartTooltip, DonutLegend, type TooltipRow }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/chart-state.ts",
      "content": "import type * as React from \"react\"\nimport { useEffect, useLayoutEffect, useRef, useState } from \"react\"\nimport { axisLabelWidth, bandPositions, labelStride, niceTicks, pointPositions, scaleLinear } from \"@/registry/miseui/ui/chart-math\"\n\nconst PLOT_TOP = 8\nconst AXIS_BOTTOM = 24\nconst AXIS_RIGHT = 8\nconst TICK_SPACING = 48\n\ninterface CartesianInput {\n  width: number\n  height: number\n  labels: readonly string[]\n  values: readonly number[]\n  band: boolean\n  includeZero: boolean\n  format: (value: number) => string\n}\n\nfunction cartesianLayout({ width, height, labels, values, band, includeZero, format }: CartesianInput) {\n  const plotHeight = Math.max(1, height - PLOT_TOP - AXIS_BOTTOM)\n  const ticks = niceTicks(values, Math.max(2, Math.round(plotHeight / TICK_SPACING)), includeZero)\n  const left = Math.max(...ticks.ticks.map((tick) => axisLabelWidth(format(tick)))) + 8\n  const plotWidth = Math.max(1, width - left - AXIS_RIGHT)\n  const count = labels.length\n  const inner = band ? bandPositions(count, plotWidth) : pointPositions(count, plotWidth)\n  const y = scaleLinear([ticks.min, ticks.max], [PLOT_TOP + plotHeight, PLOT_TOP])\n  const longest = Math.max(0, ...labels.map((label) => label.length))\n  return {\n    left,\n    right: left + plotWidth,\n    top: PLOT_TOP,\n    bottom: PLOT_TOP + plotHeight,\n    plotWidth,\n    positions: inner.map((position) => position + left),\n    bandWidth: count > 0 ? plotWidth / count : 0,\n    ticks: ticks.ticks,\n    y,\n    baseline: y(Math.min(ticks.max, Math.max(ticks.min, 0))),\n    stride: labelStride(count, plotWidth, longest),\n    band,\n  }\n}\n\ntype CartesianLayout = ReturnType<typeof cartesianLayout>\n\n/**\n * Measured before paint: ResizeObserver delivers its first entry in the same frame, so a chart\n * never shows at the fallback width. Axis text keeps its size because the SVG is redrawn, not scaled.\n */\nfunction useElementWidth(ref: React.RefObject<HTMLElement | null>, fallback: number) {\n  const [width, setWidth] = useState(fallback)\n  useLayoutEffect(() => {\n    const element = ref.current\n    if (!element || typeof ResizeObserver === \"undefined\") return\n    const observer = new ResizeObserver(([entry]) => {\n      const next = Math.round(entry.contentRect.width)\n      if (next > 0) setWidth(next)\n    })\n    observer.observe(element)\n    return () => observer.disconnect()\n  }, [ref])\n  return width\n}\n\n/** Starts seen where the browser cannot tell, so a chart is never stuck undrawn. */\nfunction useSeenOnce(ref: React.RefObject<HTMLElement | null>, enabled: boolean) {\n  const [seen, setSeen] = useState(() => typeof IntersectionObserver === \"undefined\")\n  useEffect(() => {\n    const element = ref.current\n    if (!enabled || seen || !element) return\n    const observer = new IntersectionObserver(\n      ([entry]) => {\n        if (entry.isIntersecting) setSeen(true)\n      },\n      { threshold: 0.3 },\n    )\n    observer.observe(element)\n    return () => observer.disconnect()\n  }, [ref, enabled, seen])\n  return seen\n}\n\nfunction chartKeyStep(key: string, active: number | null, count: number) {\n  if (count === 0) return undefined\n  if (key === \"Home\") return 0\n  if (key === \"End\") return count - 1\n  if (key === \"Escape\") return null\n  const from = active ?? -1\n  if (key === \"ArrowRight\" || key === \"ArrowDown\") return Math.min(count - 1, from + 1)\n  if (key === \"ArrowLeft\" || key === \"ArrowUp\") return active === null ? 0 : Math.max(0, from - 1)\n  return undefined\n}\n\ntype HitTest = (x: number, y: number) => number | null\n\nfunction useActivePoint(count: number, hit: HitTest) {\n  const [rawActive, setActive] = useState<number | null>(null)\n  const [viaKeyboard, setViaKeyboard] = useState(false)\n  const pressing = useRef(false)\n  const active = rawActive !== null && rawActive < count ? rawActive : null\n\n  const hitFrom = (event: React.PointerEvent<HTMLElement>) => {\n    const rect = event.currentTarget.getBoundingClientRect()\n    setActive(hit(event.clientX - rect.left, event.clientY - rect.top))\n    setViaKeyboard(false)\n  }\n\n  const surface = {\n    onPointerDown: (event: React.PointerEvent<HTMLElement>) => {\n      pressing.current = true\n      hitFrom(event)\n    },\n    onPointerUp: () => {\n      pressing.current = false\n    },\n    onPointerCancel: () => {\n      pressing.current = false\n      setActive(null)\n    },\n    onPointerMove: hitFrom,\n    // A finger lifting also counts as leaving; on touch the tip stays until the next tap or blur.\n    onPointerLeave: (event: React.PointerEvent<HTMLElement>) => {\n      if (event.pointerType === \"mouse\") setActive(null)\n    },\n    onFocus: () => {\n      if (pressing.current) return\n      setActive((current) => current ?? (count > 0 ? 0 : null))\n      setViaKeyboard(true)\n    },\n    onBlur: () => {\n      setActive(null)\n      setViaKeyboard(false)\n    },\n    onKeyDown: (event: React.KeyboardEvent<HTMLElement>) => {\n      const next = chartKeyStep(event.key, active, count)\n      if (next === undefined) return\n      event.preventDefault()\n      setActive(next)\n      setViaKeyboard(true)\n    },\n  }\n  return { active, viaKeyboard, surface }\n}\n\nexport { cartesianLayout, chartKeyStep, useActivePoint, useElementWidth, useSeenOnce, type CartesianLayout }\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "display"
  ],
  "type": "registry:ui"
}