{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "charts",
  "type": "registry:ui",
  "title": "Charts",
  "description": "Lightweight SVG-based telemetry charts (AreaChart, BarChart, Sparkline) with crosshair and hover tooltips.",
  "registryDependencies": [
    "https://ui.arpitbhayani.me/r/aui.json"
  ],
  "files": [
    {
      "path": "ui/charts.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface DataPoint {\n  date: string;\n  [key: string]: string | number;\n}\n\nexport interface AreaChartProps {\n  data: DataPoint[];\n  categories: string[];\n  index: string;\n  colors?: (\"primary\" | \"emerald\" | \"amber\" | \"rose\" | \"cyan\")[];\n  valueFormatter?: (value: number) => string;\n  height?: number;\n  showGrid?: boolean;\n  showLegend?: boolean;\n  className?: string;\n}\n\nexport interface BarChartProps {\n  data: DataPoint[];\n  categories: string[];\n  index: string;\n  colors?: (\"primary\" | \"emerald\" | \"amber\" | \"rose\" | \"cyan\")[];\n  valueFormatter?: (value: number) => string;\n  height?: number;\n  showGrid?: boolean;\n  showLegend?: boolean;\n  className?: string;\n}\n\nexport interface SparklineProps {\n  data: number[];\n  color?: \"primary\" | \"emerald\" | \"amber\" | \"rose\" | \"cyan\";\n  height?: number;\n  width?: number;\n  className?: string;\n}\n\nconst COLOR_MAP: Record<string, { stroke: string; fill: string }> = {\n  primary: { stroke: \"var(--primary, #e5000f)\", fill: \"rgba(229, 0, 15, 0.12)\" },\n  emerald: { stroke: \"#10b981\", fill: \"rgba(16, 185, 129, 0.12)\" },\n  amber: { stroke: \"#d97706\", fill: \"rgba(217, 119, 6, 0.12)\" },\n  rose: { stroke: \"#dc2626\", fill: \"rgba(220, 38, 38, 0.12)\" },\n  cyan: { stroke: \"#0891b2\", fill: \"rgba(8, 145, 178, 0.12)\" },\n};\n\nexport function AreaChart({\n  data = [],\n  categories = [],\n  index,\n  colors = [\"primary\", \"emerald\", \"amber\"],\n  valueFormatter = (val) => String(val),\n  height = 240,\n  showGrid = true,\n  showLegend = true,\n  className,\n}: AreaChartProps) {\n  const [hoverIndex, setHoverIndex] = React.useState<number | null>(null);\n  const svgRef = React.useRef<SVGSVGElement>(null);\n\n  if (!data || data.length === 0) {\n    return (\n      <div\n        className={cn(\"flex items-center justify-center border border-dashed border-border rounded-lg text-xs text-muted-foreground\", className)}\n        style={{ height }}\n      >\n        No telemetry data\n      </div>\n    );\n  }\n\n  const padLeft = 48;\n  const padRight = 16;\n  const padTop = 16;\n  const padBottom = 28;\n  const width = 600;\n\n  const chartW = width - padLeft - padRight;\n  const chartH = height - padTop - padBottom;\n\n  let maxVal = 0;\n  for (const row of data) {\n    for (const cat of categories) {\n      const val = Number(row[cat]) || 0;\n      if (val > maxVal) maxVal = val;\n    }\n  }\n  if (maxVal === 0) maxVal = 10;\n  maxVal = Math.ceil(maxVal * 1.1);\n\n  const getX = (idx: number) => {\n    if (data.length <= 1) return padLeft + chartW / 2;\n    return padLeft + (idx / (data.length - 1)) * chartW;\n  };\n\n  const getY = (val: number) => {\n    return padTop + chartH - (val / maxVal) * chartH;\n  };\n\n  const gridSteps = 4;\n  const yTicks = Array.from({ length: gridSteps + 1 }, (_, i) => {\n    const val = (maxVal / gridSteps) * (gridSteps - i);\n    return { val, y: padTop + (i / gridSteps) * chartH };\n  });\n\n  return (\n    <div className={cn(\"flex flex-col w-full\", className)}>\n      {showLegend && (\n        <div className=\"flex items-center gap-4 mb-2 font-mono text-xs\">\n          {categories.map((cat, i) => {\n            const colorKey = colors[i % colors.length] || \"primary\";\n            const c = COLOR_MAP[colorKey] || COLOR_MAP.primary;\n            return (\n              <div key={cat} className=\"flex items-center gap-1.5\">\n                <span className=\"h-2 w-2 rounded-full\" style={{ backgroundColor: c.stroke }} />\n                <span className=\"text-muted-foreground\">{cat}</span>\n              </div>\n            );\n          })}\n        </div>\n      )}\n\n      <div className=\"relative w-full\" style={{ height }}>\n        <svg\n          ref={svgRef}\n          viewBox={`0 0 ${width} ${height}`}\n          preserveAspectRatio=\"none\"\n          className=\"w-full h-full overflow-visible\"\n          onMouseMove={(e) => {\n            if (!svgRef.current) return;\n            const rect = svgRef.current.getBoundingClientRect();\n            const relX = ((e.clientX - rect.left) / rect.width) * width;\n            let closest = 0;\n            let minDiff = Infinity;\n            for (let i = 0; i < data.length; i++) {\n              const diff = Math.abs(getX(i) - relX);\n              if (diff < minDiff) {\n                minDiff = diff;\n                closest = i;\n              }\n            }\n            setHoverIndex(closest);\n          }}\n          onMouseLeave={() => setHoverIndex(null)}\n        >\n          {showGrid &&\n            yTicks.map((tick, i) => (\n              <g key={i}>\n                <line\n                  x1={padLeft}\n                  y1={tick.y}\n                  x2={width - padRight}\n                  y2={tick.y}\n                  stroke=\"currentColor\"\n                  className=\"text-border\"\n                  strokeDasharray=\"3 3\"\n                />\n                <text\n                  x={padLeft - 8}\n                  y={tick.y + 4}\n                  textAnchor=\"end\"\n                  className=\"font-mono text-[9px] fill-muted-foreground\"\n                >\n                  {valueFormatter(Math.round(tick.val))}\n                </text>\n              </g>\n            ))}\n\n          {categories.map((cat, i) => {\n            const colorKey = colors[i % colors.length] || \"primary\";\n            const c = COLOR_MAP[colorKey] || COLOR_MAP.primary;\n            const points = data.map((d, idx) => ({\n              x: getX(idx),\n              y: getY(Number(d[cat]) || 0),\n            }));\n\n            if (points.length === 0) return null;\n\n            const lineD = points.reduce(\n              (acc, pt, idx) => (idx === 0 ? `M ${pt.x} ${pt.y}` : `${acc} L ${pt.x} ${pt.y}`),\n              \"\"\n            );\n            const areaD = `${lineD} L ${points[points.length - 1].x} ${padTop + chartH} L ${points[0].x} ${padTop + chartH} Z`;\n\n            return (\n              <g key={cat}>\n                <defs>\n                  <linearGradient id={`reg-grad-${cat}-${i}`} x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n                    <stop offset=\"0%\" stopColor={c.stroke} stopOpacity=\"0.25\" />\n                    <stop offset=\"100%\" stopColor={c.stroke} stopOpacity=\"0.0\" />\n                  </linearGradient>\n                </defs>\n                <path d={areaD} fill={`url(#reg-grad-${cat}-${i})`} />\n                <path d={lineD} fill=\"none\" stroke={c.stroke} strokeWidth=\"2\" strokeLinecap=\"round\" />\n              </g>\n            );\n          })}\n\n          {data.length > 0 && (\n            <>\n              <text x={getX(0)} y={height - 8} textAnchor=\"start\" className=\"font-mono text-[9px] fill-muted-foreground\">\n                {String(data[0][index] || \"\")}\n              </text>\n              <text x={getX(data.length - 1)} y={height - 8} textAnchor=\"end\" className=\"font-mono text-[9px] fill-muted-foreground\">\n                {String(data[data.length - 1][index] || \"\")}\n              </text>\n            </>\n          )}\n\n          {hoverIndex !== null && data[hoverIndex] && (\n            <g>\n              <line\n                x1={getX(hoverIndex)}\n                y1={padTop}\n                x2={getX(hoverIndex)}\n                y2={padTop + chartH}\n                stroke=\"currentColor\"\n                className=\"text-border\"\n                strokeDasharray=\"2 2\"\n              />\n            </g>\n          )}\n        </svg>\n\n        {hoverIndex !== null && data[hoverIndex] && (\n          <div\n            className=\"absolute top-2 z-20 pointer-events-none rounded-md border border-border bg-popover p-2 text-xs shadow-md\"\n            style={{\n              left: `${(getX(hoverIndex) / width) * 100}%`,\n              transform: hoverIndex > data.length / 2 ? \"translate(-105%, 10%)\" : \"translate(10%, 10%)\",\n            }}\n          >\n            <div className=\"font-mono font-medium text-muted-foreground border-b border-border pb-1 mb-1\">\n              {String(data[hoverIndex][index] || \"\")}\n            </div>\n            {categories.map((cat, i) => {\n              const colorKey = colors[i % colors.length] || \"primary\";\n              const c = COLOR_MAP[colorKey] || COLOR_MAP.primary;\n              return (\n                <div key={cat} className=\"flex items-center gap-2\">\n                  <span className=\"h-1.5 w-1.5 rounded-full\" style={{ backgroundColor: c.stroke }} />\n                  <span className=\"text-foreground\">{cat}:</span>\n                  <span className=\"font-mono font-medium text-foreground\">\n                    {valueFormatter(Number(data[hoverIndex][cat]) || 0)}\n                  </span>\n                </div>\n              );\n            })}\n          </div>\n        )}\n      </div>\n    </div>\n  );\n}\n\nexport function BarChart({\n  data = [],\n  categories = [],\n  index,\n  colors = [\"primary\", \"emerald\", \"amber\"],\n  valueFormatter = (val) => String(val),\n  height = 240,\n  showGrid = true,\n  showLegend = true,\n  className,\n}: BarChartProps) {\n  if (!data || data.length === 0) {\n    return (\n      <div\n        className={cn(\"flex items-center justify-center border border-dashed border-border rounded-lg text-xs text-muted-foreground\", className)}\n        style={{ height }}\n      >\n        No telemetry data\n      </div>\n    );\n  }\n\n  const padLeft = 48;\n  const padRight = 16;\n  const padTop = 16;\n  const padBottom = 28;\n  const width = 600;\n\n  const chartW = width - padLeft - padRight;\n  const chartH = height - padTop - padBottom;\n\n  let maxVal = 0;\n  for (const row of data) {\n    for (const cat of categories) {\n      const val = Number(row[cat]) || 0;\n      if (val > maxVal) maxVal = val;\n    }\n  }\n  if (maxVal === 0) maxVal = 10;\n  maxVal = Math.ceil(maxVal * 1.15);\n\n  const groupWidth = chartW / data.length;\n  const barPadding = 0.2;\n  const availableBarWidth = groupWidth * (1 - barPadding);\n  const barWidth = Math.max(2, availableBarWidth / categories.length);\n\n  const getY = (val: number) => padTop + chartH - (val / maxVal) * chartH;\n\n  return (\n    <div className={cn(\"flex flex-col w-full\", className)}>\n      {showLegend && (\n        <div className=\"flex items-center gap-4 mb-2 font-mono text-xs\">\n          {categories.map((cat, i) => {\n            const colorKey = colors[i % colors.length] || \"primary\";\n            const c = COLOR_MAP[colorKey] || COLOR_MAP.primary;\n            return (\n              <div key={cat} className=\"flex items-center gap-1.5\">\n                <span className=\"h-2 w-2 rounded-full\" style={{ backgroundColor: c.stroke }} />\n                <span className=\"text-muted-foreground\">{cat}</span>\n              </div>\n            );\n          })}\n        </div>\n      )}\n\n      <div className=\"relative w-full\" style={{ height }}>\n        <svg viewBox={`0 0 ${width} ${height}`} preserveAspectRatio=\"none\" className=\"w-full h-full overflow-visible\">\n          {data.map((row, groupIdx) => {\n            const groupX = padLeft + groupIdx * groupWidth + (groupWidth * barPadding) / 2;\n            return (\n              <g key={groupIdx}>\n                {categories.map((cat, catIdx) => {\n                  const colorKey = colors[catIdx % colors.length] || \"primary\";\n                  const c = COLOR_MAP[colorKey] || COLOR_MAP.primary;\n                  const val = Number(row[cat]) || 0;\n                  const barX = groupX + catIdx * barWidth;\n                  const barY = getY(val);\n                  const barH = padTop + chartH - barY;\n\n                  return (\n                    <rect\n                      key={cat}\n                      x={barX}\n                      y={barY}\n                      width={Math.max(1, barWidth - 1)}\n                      height={Math.max(0, barH)}\n                      fill={c.stroke}\n                      rx=\"1\"\n                    />\n                  );\n                })}\n                <text x={groupX + availableBarWidth / 2} y={height - 8} textAnchor=\"middle\" className=\"font-mono text-[9px] fill-muted-foreground\">\n                  {String(row[index] || \"\")}\n                </text>\n              </g>\n            );\n          })}\n        </svg>\n      </div>\n    </div>\n  );\n}\n\nexport function Sparkline({\n  data = [],\n  color = \"primary\",\n  height = 32,\n  width = 96,\n  className,\n}: SparklineProps) {\n  if (!data || data.length < 2) return null;\n\n  const strokeColor = COLOR_MAP[color]?.stroke || COLOR_MAP.primary.stroke;\n  const padding = 2;\n  const drawW = width - padding * 2;\n  const drawH = height - padding * 2;\n\n  const min = Math.min(...data);\n  const max = Math.max(...data);\n  const range = max - min || 1;\n\n  const points = data.map((val, idx) => ({\n    x: padding + (idx / (data.length - 1)) * drawW,\n    y: padding + drawH - ((val - min) / range) * drawH,\n  }));\n\n  const lineD = points.reduce((acc, pt, idx) => (idx === 0 ? `M ${pt.x} ${pt.y}` : `${acc} L ${pt.x} ${pt.y}`), \"\");\n  const lastPt = points[points.length - 1];\n\n  return (\n    <svg width={width} height={height} viewBox={`0 0 ${width} ${height}`} className={cn(\"overflow-visible inline-block\", className)}>\n      <path d={lineD} fill=\"none\" stroke={strokeColor} strokeWidth=\"1.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n      <circle cx={lastPt.x} cy={lastPt.y} r=\"2.5\" fill={strokeColor} />\n    </svg>\n  );\n}\n",
      "type": "registry:ui"
    }
  ]
}