{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "chart",
  "title": "Chart",
  "description": "Chart.js charts with UX4G presentation defaults and a screen-reader data table.",
  "dependencies": ["chart.js"],
  "registryDependencies": [
    "https://indiacn.in/r/theme.json",
    "https://indiacn.in/r/typography.json"
  ],
  "files": [
    {
      "path": "components/ui/chart.tsx",
      "content": "'use client';\n\nimport {\n  Chart as ChartJS,\n  type ChartConfiguration,\n  type ChartData,\n  type ChartOptions,\n  type ChartType,\n  registerables,\n} from 'chart.js';\nimport { ComponentProps, useEffect, useId, useRef, useState } from 'react';\n\nimport { Body3, Label1 } from '@/components/ui/typography';\nimport { cn } from '@/lib/utils';\n\nChartJS.register(...registerables);\n\n/*\n * UX4G charts are Chart.js, and its page specifies presentation rather than a\n * palette — the three sample charts use three different colour sets, and one\n * legend swatch does not even match its own series. So the conventions below\n * are measured and the palette is derived from our tokens, which the mocks'\n * pastels are not.\n *\n * Measured from the Figma symbols: centred bold title, legend above the plot\n * with rectangular swatches, grid on both axes, straight line segments, thick\n * strokes and large ringed points.\n */\n\n/**\n * Watches for a theme change and returns a value that changes with it.\n *\n * Canvas colours are painted, not inherited: once Chart.js has drawn a legend\n * label in the light-mode ink, switching to dark leaves it invisible. So the\n * chart has to be told to repaint.\n */\nfunction useThemeEpoch() {\n  const [epoch, setEpoch] = useState(0);\n\n  useEffect(() => {\n    const bump = () => setEpoch(e => e + 1);\n    const observer = new MutationObserver(bump);\n    observer.observe(document.documentElement, {\n      attributes: true,\n      attributeFilter: ['class', 'style', 'data-theme'],\n    });\n    return () => observer.disconnect();\n  }, []);\n\n  return epoch;\n}\n\n/** Reads a theme token at runtime so charts follow light and dark. */\nfunction token(name: string, fallback: string) {\n  if (typeof window === 'undefined') return fallback;\n  const value = getComputedStyle(document.documentElement).getPropertyValue(name).trim();\n  return value || fallback;\n}\n\n/**\n * Series palettes, split by how a type draws.\n *\n * Fill types get primary and primary-100, which is what the Figma bar chart\n * measures — a pale tint reads fine over a large area. Stroke types cannot use\n * it: primary-100 as a 4px line is nearly invisible, which is why the radar\n * chart's second series could not be seen. Figma's own line chart pairs two\n * distinct hues rather than a tint, so stroke types walk the semantic scales.\n */\nfunction chartPalette(stroke = false) {\n  const primary = token('--primary', '#613af5');\n  const semantic = [\n    token('--info', '#006699'),\n    token('--success', '#107400'),\n    token('--warning', '#a46212'),\n    token('--danger', '#b7131a'),\n    token('--secondary', '#746d96'),\n  ];\n\n  return stroke\n    ? [primary, ...semantic]\n    : [primary, token('--primary-100', '#ecd0ff'), ...semantic, token('--primary-300', '#c495ff')];\n}\n\nconst ARC_TYPES = new Set(['pie', 'doughnut', 'polarArea']);\n\n/*\n * Types whose datasets are drawn as overlapping filled shapes. An opaque fill\n * here hides every series behind the largest one, which is how the radar chart\n * shipped with only one of its two series visible.\n */\nconst AREA_TYPES = new Set(['radar', 'polarArea']);\n\n/** Adds alpha to a hex token, leaving any other colour format untouched. */\nfunction withAlpha(colour: string, alpha: number) {\n  if (!/^#[0-9a-fA-F]{6}$/.test(colour)) return colour;\n  const hex = Math.round(alpha * 255)\n    .toString(16)\n    .padStart(2, '0');\n  return `${colour}${hex}`;\n}\n\n/** UX4G presentation defaults, merged under whatever the caller passes. */\nfunction baseOptions(type: ChartType, animate: boolean): ChartOptions {\n  const ink = token('--neutral', '#212121');\n  const muted = token('--neutral-600', '#727272');\n  const grid = token('--neutral-100', '#dddddd');\n  const surface = token('--neutral-0', '#ffffff');\n  const font = { family: 'inherit', size: 12 };\n\n  const scales =\n    ARC_TYPES.has(type) || type === 'radar'\n      ? undefined\n      : {\n          x: {\n            grid: { color: grid },\n            border: { display: false },\n            ticks: { color: muted, font, maxRotation: 45, minRotation: 0 },\n          },\n          y: {\n            grid: { color: grid },\n            border: { display: false },\n            ticks: { color: muted, font },\n          },\n        };\n\n  return {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: animate ? undefined : false,\n    interaction: { mode: 'index', intersect: false },\n    plugins: {\n      legend: {\n        position: 'top',\n        align: 'center',\n        labels: {\n          color: ink,\n          font,\n          boxWidth: 40,\n          boxHeight: 16,\n          // Rectangular swatches, as the Figma legend draws them.\n          usePointStyle: false,\n          padding: 20,\n        },\n      },\n      tooltip: {\n        backgroundColor: ink,\n        titleColor: surface,\n        bodyColor: surface,\n        borderColor: grid,\n        borderWidth: 1,\n        padding: 10,\n        cornerRadius: 8,\n        displayColors: true,\n        titleFont: font,\n        bodyFont: font,\n      },\n    },\n    elements: {\n      line: { tension: 0, borderWidth: 4 },\n      point: { radius: 7, borderWidth: 3, hoverRadius: 9 },\n      bar: { borderWidth: 0 },\n      arc: { borderWidth: 2, borderColor: surface },\n    },\n    scales,\n  } as ChartOptions;\n}\n\n/** Applies the palette to any dataset that has not chosen its own colours. */\nfunction paintDatasets(type: ChartType, data: ChartData): ChartData {\n  const arc = ARC_TYPES.has(type);\n  // Bars are filled shapes too, so they keep the Figma tint pair.\n  const strokeDrawn = !arc && type !== 'bar';\n  const palette = chartPalette(strokeDrawn);\n\n  const area = AREA_TYPES.has(type);\n\n  return {\n    ...data,\n    datasets: data.datasets.map((dataset, i) => {\n      const colour = palette[i % palette.length];\n      const perPoint = arc\n        ? (dataset.data as unknown[]).map((_, j) => palette[j % palette.length])\n        : colour;\n\n      return {\n        // Overlapping fills have to be translucent or they hide each other.\n        backgroundColor: area ? withAlpha(colour, 0.35) : perPoint,\n        borderColor: arc && !area ? undefined : colour,\n        pointBackgroundColor: colour,\n        pointBorderColor: token('--neutral-0', '#ffffff'),\n        ...dataset,\n      };\n    }),\n  };\n}\n\ninterface IChartProps extends Omit<ComponentProps<'figure'>, 'children'> {\n  type: ChartType;\n  data: ChartData;\n  /** Required: a canvas is invisible to assistive technology without it. */\n  title: string;\n  options?: ChartOptions;\n  /** One sentence on what the chart shows. Read out before the data table. */\n  summary?: string;\n  height?: number;\n}\n\n/**\n * Chart.js chart with UX4G presentation defaults.\n *\n * The canvas is `aria-hidden` and the same data is rendered as a\n * visually-hidden table, because a canvas conveys nothing to a screen reader.\n * A chart that only works visually is not finished.\n */\nfunction Chart({\n  className,\n  type,\n  data,\n  options,\n  title,\n  summary,\n  height = 320,\n  ...props\n}: IChartProps) {\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const chartRef = useRef<ChartJS | null>(null);\n  const tableId = useId();\n  const [animate, setAnimate] = useState(true);\n  const themeEpoch = useThemeEpoch();\n\n  useEffect(() => {\n    const query = window.matchMedia('(prefers-reduced-motion: reduce)');\n    const sync = () => setAnimate(!query.matches);\n    sync();\n    query.addEventListener('change', sync);\n    return () => query.removeEventListener('change', sync);\n  }, []);\n\n  useEffect(() => {\n    const canvas = canvasRef.current;\n    if (!canvas) return;\n\n    const config: ChartConfiguration = {\n      type,\n      data: paintDatasets(type, data),\n      options: { ...baseOptions(type, animate), ...options },\n    };\n\n    const chart = new ChartJS(canvas, config);\n    chartRef.current = chart;\n    return () => {\n      chart.destroy();\n      chartRef.current = null;\n    };\n    // themeEpoch is in here on purpose: a theme change has to repaint the canvas.\n  }, [type, data, options, animate, themeEpoch]);\n\n  const labels = (data.labels ?? []) as string[];\n\n  return (\n    <figure className={cn('bg-neutral-0 m-0 w-full', className)} {...props}>\n      <Label1 className='text-neutral mb-3 block text-center font-semibold'>{title}</Label1>\n      <div style={{ height }} className='relative w-full'>\n        <canvas ref={canvasRef} aria-hidden />\n      </div>\n\n      {/* The accessible equivalent of the canvas above. */}\n      <figcaption className='sr-only'>\n        {summary && <Body3>{summary}</Body3>}\n        <table id={tableId}>\n          <caption>{title}</caption>\n          <thead>\n            <tr>\n              <th scope='col'>Category</th>\n              {data.datasets.map((dataset, i) => (\n                <th key={i} scope='col'>\n                  {dataset.label ?? `Series ${i + 1}`}\n                </th>\n              ))}\n            </tr>\n          </thead>\n          <tbody>\n            {labels.map((label, row) => (\n              <tr key={label}>\n                <th scope='row'>{label}</th>\n                {data.datasets.map((dataset, i) => (\n                  <td key={i}>{String((dataset.data as unknown[])[row] ?? '')}</td>\n                ))}\n              </tr>\n            ))}\n          </tbody>\n        </table>\n      </figcaption>\n    </figure>\n  );\n}\n\nexport { Chart, chartPalette };\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}
