{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "echarts-chart",
  "dependencies": [
    "echarts"
  ],
  "files": [
    {
      "path": "src/registry/ui/echarts-chart.tsx",
      "content": "import type { ComponentType, ReactNode } from \"react\";\nimport * as echarts from \"echarts/core\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Theme keys + config — replicated from the repo's <ChartStyle> so the ECharts\n// charts stay self-contained (no recharts ui imports). Shared by every ECharts\n// chart and the tooltip/legend/brush ui modules.\n// ─────────────────────────────────────────────────────────────────────────────\n\n// Theme selectors mirror the repo's <ChartStyle>: light is the bare root, dark is `.dark`.\nexport const THEMES = { light: \"\", dark: \".dark\" } as const;\nexport type ThemeKey = keyof typeof THEMES;\nexport const THEME_KEYS = Object.keys(THEMES) as ThemeKey[];\n\n// Require at least one theme key — identical constraint to the repo's ChartConfig.\nexport type AtLeastOneThemeColor =\n  | { light: string[]; dark?: string[] }\n  | { light?: string[]; dark: string[] };\n\nexport type ChartConfig = Record<\n  string,\n  {\n    label?: ReactNode;\n    icon?: ComponentType;\n    colors?: AtLeastOneThemeColor;\n  }\n>;\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Color plumbing — replicated from the repo's <ChartStyle> so the charts stay\n// self-contained (no @/registry/ui/recharts imports).\n// ─────────────────────────────────────────────────────────────────────────────\n\n// Max slots a key needs = longest color array across themes (min 1). Both themes\n// always emit the same number of `--color-{key}-{n}` vars.\nexport function getColorsCount(item: ChartConfig[string]): number {\n  if (!item.colors) return 1;\n  const counts = THEME_KEYS.map((theme) => item.colors?.[theme]?.length ?? 0);\n  return Math.max(...counts, 1);\n}\n\n// Distribute colors evenly across slots; extra slots go to the LAST color(s).\n// 2 colors / 4 slots → [c0, c0, c1, c1]; 3 colors / 4 slots → [c0, c1, c2, c2].\nexport function distributeColors(colors: string[], maxCount: number): string[] {\n  const available = colors.length;\n  if (available >= maxCount) return colors.slice(0, maxCount);\n\n  const result: string[] = [];\n  const baseSlots = Math.floor(maxCount / available);\n  const extraSlots = maxCount % available;\n\n  for (let i = 0; i < available; i++) {\n    const isExtra = i >= available - extraSlots;\n    const slots = baseSlots + (isExtra ? 1 : 0);\n    for (let j = 0; j < slots; j++) result.push(colors[i]);\n  }\n\n  return result;\n}\n\n// Emits the same CSS <ChartStyle> would: `--color-{key}-{n}` scoped to\n// `[data-chart={id}]` (light) and `.dark [data-chart={id}]` (dark).\nexport function buildChartCss(id: string, config: ChartConfig): string {\n  const colorConfig = Object.entries(config).filter(([, item]) => item.colors);\n  if (!colorConfig.length) return \"\";\n\n  const varsFor = (theme: ThemeKey) =>\n    colorConfig\n      .flatMap(([key, item]) => {\n        const authored = item.colors?.[theme];\n        if (!authored || authored.length === 0) return [];\n        return distributeColors(authored, getColorsCount(item)).map(\n          (color, index) => `  --color-${key}-${index}: ${color};`,\n        );\n      })\n      .join(\"\\n\");\n\n  return Object.entries(THEMES)\n    .map(([theme, prefix]) => `${prefix} [data-chart=${id}] {\\n${varsFor(theme as ThemeKey)}\\n}`)\n    .join(\"\\n\");\n}\n\n// A single reusable 1×1 canvas normalizes ANY CSS color (hex, named, oklch, …)\n// to a concrete rgba string by painting it and reading the pixel back.\nlet normalizerCtx: CanvasRenderingContext2D | null = null;\nexport function normalizeColor(value: string): string {\n  const raw = value.trim();\n  if (!raw || typeof document === \"undefined\") return raw;\n\n  if (!normalizerCtx) {\n    const canvas = document.createElement(\"canvas\");\n    canvas.width = 1;\n    canvas.height = 1;\n    normalizerCtx = canvas.getContext(\"2d\", { willReadFrequently: true });\n  }\n  if (!normalizerCtx) return raw;\n\n  normalizerCtx.clearRect(0, 0, 1, 1);\n  normalizerCtx.fillStyle = \"#000\";\n  normalizerCtx.fillStyle = raw; // invalid values leave the sentinel in place\n  normalizerCtx.fillRect(0, 0, 1, 1);\n  const [r, g, b, a] = normalizerCtx.getImageData(0, 0, 1, 1).data;\n  return `rgba(${r}, ${g}, ${b}, ${(a / 255).toFixed(3)})`;\n}\n\n// Scales the alpha of a normalized `rgba(r, g, b, a)` string. Multiplying (not\n// replacing) keeps translucent theme tokens honest: a border that is 10%-white\n// at `withAlpha(border, 0.5)` lands at 5%, matching Tailwind's `border/50`.\nexport function withAlpha(color: string, alpha: number): string {\n  const match = color.match(/rgba?\\(([^)]+)\\)/);\n  if (!match) return color;\n  const [r, g, b, a] = match[1].split(\",\").map((p) => p.trim());\n  const base = a === undefined ? 1 : Number.parseFloat(a) || 0;\n  return `rgba(${r}, ${g}, ${b}, ${(base * alpha).toFixed(3)})`;\n}\n\nexport type ResolvedColors = {\n  series: Record<string, string[]>; // normalized `--color-{key}-{n}` slots per key\n  tokens: {\n    mutedForeground: string;\n    border: string;\n    foreground: string;\n    background: string;\n  };\n};\n\n// Reads the injected CSS vars + theme tokens from the live DOM. Series slots come\n// from `getComputedStyle` on the container; tokens are read off a throwaway probe\n// carrying the matching Tailwind class (robust to the var naming a theme uses).\nexport function resolveColors(\n  container: HTMLElement,\n  config: ChartConfig,\n  seriesKeys: string[],\n): ResolvedColors {\n  const computed = getComputedStyle(container);\n  const series: Record<string, string[]> = {};\n\n  for (const key of seriesKeys) {\n    const count = getColorsCount(config[key] ?? {});\n    const slots: string[] = [];\n    for (let n = 0; n < count; n++) {\n      const raw = computed.getPropertyValue(`--color-${key}-${n}`).trim();\n      slots.push(raw ? normalizeColor(raw) : \"rgba(120, 120, 120, 1)\");\n    }\n    series[key] = slots;\n  }\n\n  const probe = document.createElement(\"span\");\n  probe.style.cssText = \"position:absolute;width:0;height:0;visibility:hidden;pointer-events:none;\";\n  container.appendChild(probe);\n  const readToken = (className: string) => {\n    probe.className = className;\n    return normalizeColor(getComputedStyle(probe).color);\n  };\n  const tokens = {\n    mutedForeground: readToken(\"text-muted-foreground\"),\n    border: readToken(\"text-border\"),\n    foreground: readToken(\"text-foreground\"),\n    background: readToken(\"text-background\"),\n  };\n  container.removeChild(probe);\n\n  return { series, tokens };\n}\n\n// Horizontal multi-stop color for a series — a solid string when there is only\n// one color, else an evenly-distributed left→right LinearGradient. Reused for the\n// stroke, symbol fills, and as the base tint for the area fill.\nexport function seriesPaint(slots: string[]): string | echarts.graphic.LinearGradient {\n  if (slots.length <= 1) return slots[0] ?? \"rgba(120, 120, 120, 1)\";\n  const stops = slots.map((color, i) => ({ offset: i / (slots.length - 1), color }));\n  return new echarts.graphic.LinearGradient(0, 0, 1, 0, stops);\n}\n\n// Solid var / gradient of vars for a series indicator — mirrors getIndicatorColorStyle.\n// Used by BOTH the tooltip rows and the legend indicators.\nexport function indicatorBackground(key: string, colorsCount: number): string {\n  if (colorsCount <= 1) return `var(--color-${key}-0)`;\n  const stops = Array.from({ length: colorsCount }, (_, i) => {\n    const offset = (i / (colorsCount - 1)) * 100;\n    return `var(--color-${key}-${i}) ${offset}%`;\n  }).join(\", \");\n  return `linear-gradient(to right, ${stops})`;\n}\n\n// Composites a translucent color over an opaque base into a FLAT color. The\n// tick dots need this: a translucent stroke double-paints where its round caps\n// overlap the line body, which reads as two stacked colors.\nexport function flattenColor(color: string, base: string): string {\n  const parse = (value: string) =>\n    value\n      .match(/rgba?\\(([^)]+)\\)/)?.[1]\n      .split(\",\")\n      .map((part) => Number.parseFloat(part)) ?? [0, 0, 0, 1];\n  const [r, g, b, a = 1] = parse(color);\n  const [baseR, baseG, baseB] = parse(base);\n  const mix = (channel: number, baseChannel: number) =>\n    Math.round(channel * a + baseChannel * (1 - a));\n  return `rgb(${mix(r, baseR)}, ${mix(g, baseG)}, ${mix(b, baseB)})`;\n}\n",
      "type": "registry:component",
      "target": "components/evilcharts/ui/echarts-chart.tsx"
    }
  ],
  "type": "registry:component"
}