{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "echarts-radial-chart",
  "description": "Radial chart component rendered with Apache ECharts",
  "dependencies": [
    "echarts",
    "motion"
  ],
  "registryDependencies": [
    "@evilcharts/echarts-chart",
    "@evilcharts/echarts-tooltip",
    "@evilcharts/echarts-legend"
  ],
  "files": [
    {
      "path": "src/registry/charts/echarts-radial-chart.tsx",
      "content": "\"use client\";\n\nimport {\n  resolveTooltipPosition,\n  roundnessClass,\n  tooltipIndicatorHtml,\n  tooltipRow,\n  tooltipVariantClass,\n  type TooltipPosition,\n  type TooltipRoundness,\n  type TooltipVariant,\n} from \"@/registry/ui/echarts-tooltip\";\nimport {\n  buildChartCss,\n  getColorsCount,\n  resolveColors,\n  withAlpha,\n  type ChartConfig,\n  type ResolvedColors,\n} from \"@/registry/ui/echarts-chart\";\nimport {\n  Children,\n  isValidElement,\n  useCallback,\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n  type FC,\n  type ReactNode,\n} from \"react\";\nimport {\n  PolarComponent,\n  TooltipComponent,\n  type PolarComponentOption,\n  type TooltipComponentOption,\n} from \"echarts/components\";\nimport { LegendIndicator, type LegendVariant } from \"@/registry/ui/echarts-legend\";\nimport { BarChart, type BarSeriesOption } from \"echarts/charts\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport { CanvasRenderer } from \"echarts/renderers\";\nimport type { ComposeOption } from \"echarts/core\";\nimport * as echarts from \"echarts/core\";\n\n// Re-export the shared types that were previously declared inline here, so\n// existing consumers/examples keep importing them from the chart module.\nexport type { ChartConfig, LegendVariant, TooltipPosition, TooltipRoundness, TooltipVariant };\n\n// Modular registration keeps the bundle lean — only the pieces this chart needs.\n// `PolarComponent` bundles the polar coordinate system together with its\n// `angleAxis` + `radiusAxis` (concentric rings are a polar `bar` series whose\n// value drives the sweep angle). No GraphicComponent: unlike the area chart's\n// brush, nothing here draws raw zrender overlays — selection is per-datum\n// opacity, and the skeleton shimmer is a swept gradient.\necharts.use([BarChart, PolarComponent, TooltipComponent, CanvasRenderer]);\n\ntype EChartsInstance = ReturnType<typeof echarts.init>;\n\n// The exact option surface this chart uses — a polar bar series plus the polar\n// component (which carries angle/radius axes as dependencies) and the tooltip.\n// Narrower than echarts' full EChartsOption, so a misspelled key fails the\n// compile instead of silently reaching setOption.\ntype EChartsOption = ComposeOption<BarSeriesOption | TooltipComponentOption | PolarComponentOption>;\n\n// Single-entry views of the composed option's array-or-single fields — the\n// modular entry points don't export the polar/axis option types directly, so\n// they are recovered from the composed option (angleAxis/radiusAxis enter it as\n// dependencies of the polar component).\ntype ArrayItem<T> = T extends readonly (infer U)[] ? U : T;\ntype PolarOption = ArrayItem<NonNullable<EChartsOption[\"polar\"]>>;\ntype AngleAxisOption = ArrayItem<NonNullable<EChartsOption[\"angleAxis\"]>>;\ntype RadiusAxisOption = ArrayItem<NonNullable<EChartsOption[\"radiusAxis\"]>>;\n\n// Per-datum bar paint — structurally assignable to the per-datum itemStyle. A\n// gradient `color` reproduces each bar's diagonal fill.\ntype BarItemStyle = {\n  color?: string | echarts.graphic.LinearGradient;\n  opacity?: number;\n};\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Constants\n// ─────────────────────────────────────────────────────────────────────────────\n\nconst DEFAULT_INNER_RADIUS = \"30%\";\nconst DEFAULT_OUTER_RADIUS = \"100%\";\nconst DEFAULT_CORNER_RADIUS = 5;\nconst DEFAULT_BAR_SIZE = 14;\nconst LOADING_BARS = 5; // skeleton ring count — matches the Recharts twin\nconst LOADING_MAX = 100; // fixed angle-axis extent while loading (values roll in a 40–100 band)\nconst LOADING_ANIMATION_DURATION = 2000; // shimmer sweep loop, in milliseconds\nconst REVEAL_DURATION = 1000; // intro sweep-in length, in milliseconds\n// NOTE: the intro draw-in runs ECharts' RAW default bar entrance (each ring\n// sweeps out from the start angle). Like the area twin, it plays on the FIRST\n// real push only; every later push (selection, theme) sends animation:false\n// because notMerge would otherwise replay the entrance on each change.\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Theme knobs — every neutral tone draws from these. Base colors come from the\n// consumer's CSS tokens (resolved off the live DOM), so only the opacity factors\n// live here. Factors MULTIPLY the token's own alpha — a border token that is\n// already 10%-white stays subtle. Tune here, not in the builder.\n// ─────────────────────────────────────────────────────────────────────────────\nconst TRACK_OPACITY = 0.15; // unfilled background ring, × muted-foreground alpha\nconst SELECTED_DIM_OPACITY = 0.15; // unselected bars while a selection is active\n// The skeleton track stays faintly visible; a bright band is CLIPPED to a small\n// sweeping window (only the arc slice inside it exists) and swept diagonally\n// across the rings, like a clip-path sliding over the chart.\nconst LOADING_SHIMMER_MAX_OPACITY = 0.4; // shimmer arc peak, × foreground alpha\nconst LOADING_SHIMMER_BAND = 0.2; // window half-width, fraction of the sweep axis\nconst LOADING_SHIMMER_FEATHER = 0.2; // eased edge softening of the clip window\n\n// Stable series ids. `__`-prefixed ids are internal (background track, skeleton)\n// and stay silent; the main ring series is the only one that reports clicks and\n// feeds the tooltip.\nconst MAIN_SERIES_ID = \"radial-bars\";\nconst TRACK_SERIES_ID = \"__track\";\n// The track rides a second, identical polar so it never shares a bar band with\n// the data rings — see buildTrackSeries for why that matters.\nconst TRACK_POLAR_INDEX = 1;\nconst LOADING_SERIES_ID = \"__loading\";\nconst LOADING_TRACK_ID = \"__loading-track\";\n\nconst FALLBACK_COLOR = \"rgba(120, 120, 120, 1)\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Public types\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport type RadialVariant = \"full\" | \"semi\";\n// TooltipVariant, TooltipRoundness, TooltipPosition, LegendVariant, and\n// ChartConfig now live in the shared @/registry/ui/echarts/* modules and are\n// imported + re-exported at the top of this file.\n\nexport interface EChartsRadialChartProps<TData extends Record<string, unknown>> {\n  data: TData[]; // rows rendered by the chart — one bar (ring) per row\n  config: ChartConfig; // bar colors + labels, keyed by each bar's name\n  nameKey: keyof TData & string; // data key holding each bar's name\n  className?: string; // extra classes for the chart container\n  variant?: RadialVariant; // arc shape — full circle or half circle\n  // Value a full sweep represents. Without it the scale is derived from the data,\n  // so the largest bar always fills the arc — set it (e.g. 100) for gauges, where\n  // a single value has to read against a fixed total.\n  max?: number;\n  innerRadius?: number | string; // inner radius of the radial bars\n  outerRadius?: number | string; // outer radius of the radial bars\n  defaultSelectedDataKey?: string | null; // bar selected on first render\n  onSelectionChange?: (selection: { dataKey: string; value: number } | null) => void; // fires when the selected bar changes\n  isLoading?: boolean; // shows the animated loading skeleton\n  backgroundVariant?: BackgroundVariant; // decorative pattern behind the chart\n  chartOptions?: Record<string, unknown>; // escape hatch merged over the built ECharts option\n  children?: ReactNode; // declarative config — <RadialBar>, <Tooltip>, <Legend>\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Composible parts — DECLARATIVE CONFIG. Every part renders `null`; the root\n// walks `children` by reference (child.type === RadialBar, …) to collect its\n// props. Presence semantics mirror the Recharts twin: omit a child and that part\n// does not render. These are never mounted into the tree — they only carry props.\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface RadialBarProps {\n  dataKey: string; // value key — determines each bar's arc length\n  cornerRadius?: number; // rounding of each bar's ends (mapped to a rounded cap)\n  barSize?: number; // thickness of each radial bar in pixels\n  showBackground?: boolean; // renders the unfilled track behind each bar\n  isClickable?: boolean; // lets bars be selected by clicking them\n}\n\n/**\n * The radial bar series. Each data row becomes one concentric ring. Pass\n * `isClickable` to make bars selectable. Renders nothing — the root reads these\n * props to build the series.\n */\nconst RadialBar: FC<RadialBarProps> = () => null;\n\nexport interface TooltipProps {\n  variant?: TooltipVariant; // visual style of the tooltip surface\n  roundness?: TooltipRoundness; // border-radius of the tooltip\n  defaultIndex?: number; // data index shown by default with no hover\n  position?: TooltipPosition; // \"variable\" follows the pointer (default); \"fixed\" pins the tooltip near the top and tracks the pointer's X\n}\n\n/** Presence enables the hover tooltip. Renders nothing. */\nconst Tooltip: FC<TooltipProps> = () => null;\n\nexport interface LegendProps {\n  variant?: LegendVariant; // visual style of the legend indicators\n  align?: \"left\" | \"center\" | \"right\"; // horizontal placement\n  verticalAlign?: \"top\" | \"middle\" | \"bottom\"; // vertical placement\n  isClickable?: boolean; // lets each entry toggle selection of its bar\n}\n\n/** Presence enables the HTML legend overlay. Renders nothing. */\nconst Legend: FC<LegendProps> = () => null;\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Children collection — walk the declarative config into plain objects the\n// option builder consumes. A missing part becomes an absent slot.\n// ─────────────────────────────────────────────────────────────────────────────\n\ntype RadialBarSlot = {\n  present: boolean;\n  dataKey: string;\n  cornerRadius: number;\n  barSize: number;\n  showBackground: boolean;\n  isClickable: boolean;\n};\ntype TooltipSlot = {\n  present: boolean;\n  variant: TooltipVariant;\n  roundness: TooltipRoundness;\n  defaultIndex?: number;\n  position: TooltipPosition;\n};\ntype LegendSlot = {\n  present: boolean;\n  variant: LegendVariant;\n  align: \"left\" | \"center\" | \"right\";\n  verticalAlign: \"top\" | \"middle\" | \"bottom\";\n  isClickable: boolean;\n};\n\ntype CollectedConfig = {\n  radialBar: RadialBarSlot;\n  tooltip: TooltipSlot;\n  legend: LegendSlot;\n};\n\nfunction collectConfig(children: ReactNode): CollectedConfig {\n  // Defaults hold even when <RadialBar> is omitted, so the loading skeleton\n  // (which needs a bar size) always has sane geometry.\n  let radialBar: RadialBarSlot = {\n    present: false,\n    dataKey: \"\",\n    cornerRadius: DEFAULT_CORNER_RADIUS,\n    barSize: DEFAULT_BAR_SIZE,\n    showBackground: true,\n    isClickable: false,\n  };\n  let tooltip: TooltipSlot = {\n    present: false,\n    variant: \"default\",\n    roundness: \"lg\",\n    position: \"variable\",\n  };\n  let legend: LegendSlot = {\n    present: false,\n    variant: \"rounded-square\",\n    align: \"center\",\n    verticalAlign: \"bottom\",\n    isClickable: false,\n  };\n\n  Children.forEach(children, (child) => {\n    if (!isValidElement(child)) return;\n    const type = child.type;\n\n    if (type === RadialBar) {\n      const props = child.props as RadialBarProps;\n      radialBar = {\n        present: true,\n        dataKey: props.dataKey,\n        cornerRadius: props.cornerRadius ?? DEFAULT_CORNER_RADIUS,\n        barSize: props.barSize ?? DEFAULT_BAR_SIZE,\n        showBackground: props.showBackground ?? true,\n        isClickable: props.isClickable ?? false,\n      };\n    } else if (type === Tooltip) {\n      const props = child.props as TooltipProps;\n      tooltip = {\n        present: true,\n        variant: props.variant ?? \"default\",\n        roundness: props.roundness ?? \"lg\",\n        defaultIndex: props.defaultIndex,\n        position: props.position ?? \"variable\",\n      };\n    } else if (type === Legend) {\n      const props = child.props as LegendProps;\n      legend = {\n        present: true,\n        variant: props.variant ?? \"rounded-square\",\n        align: props.align ?? \"center\",\n        verticalAlign: props.verticalAlign ?? \"bottom\",\n        isClickable: props.isClickable ?? false,\n      };\n    }\n  });\n\n  return { radialBar, tooltip, legend };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Color plumbing (ChartConfig, getColorsCount, distributeColors, buildChartCss,\n// normalizeColor, withAlpha, ResolvedColors, resolveColors, indicatorBackground)\n// now lives in @/registry/ui/echarts-chart and is imported at the top of this\n// file. Only barPaint below is chart-specific — see its note.\n// ─────────────────────────────────────────────────────────────────────────────\n\n// Diagonal multi-stop color for a bar — a solid string when there is only one\n// color, else a top-left→bottom-right LinearGradient across the bar's bounding\n// box. Mirrors the Recharts `<linearGradient x1=0 y1=0 x2=1 y2=1>` applied to\n// every sector.\nfunction barPaint(slots: string[]): string | echarts.graphic.LinearGradient {\n  if (slots.length <= 1) return slots[0] ?? FALLBACK_COLOR;\n  const stops = slots.map((color, i) => ({ offset: i / (slots.length - 1), color }));\n  return new echarts.graphic.LinearGradient(0, 0, 1, 1, stops);\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Misc helpers\n// ─────────────────────────────────────────────────────────────────────────────\n\n// Angle + center configuration for the chart's arc shape. Angles follow the\n// Recharts twin: `full` sweeps a whole clockwise circle from the top; `semi`\n// domes over the top from left to right (center pushed low, like cy=\"70%\").\n// ECharts' `clockwise: true` makes rising values rotate clockwise on screen.\nfunction getVariantGeometry(variant: RadialVariant): {\n  center: [string, string];\n  startAngle: number;\n  endAngle: number;\n} {\n  switch (variant) {\n    case \"semi\":\n      return { center: [\"50%\", \"70%\"], startAngle: 180, endAngle: 0 };\n    case \"full\":\n    default:\n      return { center: [\"50%\", \"50%\"], startAngle: 90, endAngle: -270 };\n  }\n}\n\n// A \"nice\" ceiling for the angle-axis max, so the largest ring stops just shy of\n// a full wrap (mirrors the auto-scaled domain the Recharts twin derives instead\n// of letting the biggest bar close into an ambiguous full circle). ~ECharts'\n// default value-axis nicing for a 5-way split.\nfunction niceCeil(value: number): number {\n  if (!Number.isFinite(value) || value <= 0) return 1;\n  const rough = value / 5;\n  const power = Math.floor(Math.log10(rough));\n  const base = Math.pow(10, power);\n  const fraction = rough / base;\n  const niceFraction = fraction < 1.5 ? 1 : fraction < 3 ? 2 : fraction < 7 ? 5 : 10;\n  const interval = niceFraction * base;\n  return Math.ceil(value / interval) * interval;\n}\n\n// Skeleton ring values as a smooth random walk in a comfortable band — reads\n// like a resting chart instead of raw noise.\nfunction getLoadingData(count: number): number[] {\n  const rows: number[] = [];\n  let value = 55 + Math.random() * 30;\n  for (let i = 0; i < count; i++) {\n    value = Math.min(LOADING_MAX, Math.max(40, value + (Math.random() - 0.5) * 30));\n    rows.push(Math.round(value));\n  }\n  return rows;\n}\n\n// Gradient stops forming a hard clip window around `center`: full `peak` alpha\n// inside, zero outside, with a small sine feather so the edge isn't aliased.\n// `center` may run outside [0, 1] so the window fully enters and exits the frame.\nfunction shimmerWindowStops(center: number, color: string, peak: number) {\n  const half = LOADING_SHIMMER_BAND;\n  const feather = LOADING_SHIMMER_FEATHER;\n\n  const alphaAt = (x: number) => {\n    const dist = Math.abs(x - center);\n    if (dist <= half - feather) return peak;\n    if (dist >= half) return 0;\n    // Sine-eased falloff — a linear ramp still reads as a hard cut.\n    return peak * Math.sin(((1 - (dist - (half - feather)) / feather) * Math.PI) / 2);\n  };\n\n  const offsets = [\n    0,\n    center - half,\n    center - half + feather,\n    center,\n    center + half - feather,\n    center + half,\n    1,\n  ]\n    .filter((x) => x >= 0 && x <= 1)\n    .sort((a, b) => a - b);\n\n  const stops: { offset: number; color: string }[] = [];\n  for (const offset of offsets) {\n    if (stops.length === 0 || offset - stops[stops.length - 1].offset > 1e-4) {\n      stops.push({ offset, color: withAlpha(color, alphaAt(offset)) });\n    }\n  }\n  return stops;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Tooltip / legend HTML primitives (roundnessClass, tooltipVariantClass,\n// indicatorBackground, legendFillStyle, legendOutlineStyle, LegendIndicator) now\n// live in the shared @/registry/ui/echarts/{tooltip,legend} modules and are\n// imported at the top of this file. The tooltip shell + item row below compose\n// those shared primitives; the legend overlay uses the shared LegendIndicator.\n// ─────────────────────────────────────────────────────────────────────────────\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Background patterns — a self-contained port of <ChartBackground>. The Recharts\n// twin draws these inside a Recharts <ZIndexLayer>; canvas has no such layer, so\n// here they render as a plain SVG overlay sitting behind the transparent ECharts\n// canvas. Same patterns, same `text-border` tint, same soft edge-fade mask.\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport type BackgroundVariant =\n  | \"dots\"\n  | \"grid\"\n  | \"cross-hatch\"\n  | \"diagonal-lines\"\n  | \"plus\"\n  | \"falling-triangles\"\n  | \"4-pointed-star\"\n  | \"tiny-checkers\"\n  | \"overlapping-circles\"\n  | \"wiggle-lines\"\n  | \"bubbles\";\n\ntype PatternProps = { id: string };\n\nconst BACKGROUND_PATTERNS: Record<BackgroundVariant, FC<PatternProps>> = {\n  dots: ({ id }) => (\n    <pattern id={id} x=\"0\" y=\"0\" width=\"20\" height=\"20\" patternUnits=\"userSpaceOnUse\">\n      <circle className=\"text-border\" cx=\"2\" cy=\"2\" r=\"1\" fill=\"currentColor\" />\n    </pattern>\n  ),\n  grid: ({ id }) => (\n    <pattern id={id} x=\"0\" y=\"0\" width=\"20\" height=\"20\" patternUnits=\"userSpaceOnUse\">\n      <path\n        className=\"text-border\"\n        d=\"M 20 0 L 0 0 0 20\"\n        fill=\"none\"\n        stroke=\"currentColor\"\n        strokeWidth=\"0.5\"\n      />\n    </pattern>\n  ),\n  \"cross-hatch\": ({ id }) => (\n    <pattern id={id} x=\"0\" y=\"0\" width=\"20\" height=\"20\" patternUnits=\"userSpaceOnUse\">\n      <path\n        className=\"text-border/60 dark:text-border/50\"\n        d=\"M 0 0 L 20 20 M 20 0 L 0 20\"\n        fill=\"none\"\n        stroke=\"currentColor\"\n        strokeWidth=\"0.5\"\n      />\n    </pattern>\n  ),\n  \"diagonal-lines\": ({ id }) => (\n    <pattern\n      id={id}\n      x=\"0\"\n      y=\"0\"\n      width=\"6\"\n      height=\"6\"\n      patternUnits=\"userSpaceOnUse\"\n      patternTransform=\"rotate(45)\"\n    >\n      <line\n        className=\"text-border\"\n        x1=\"0\"\n        y1=\"0\"\n        x2=\"0\"\n        y2=\"6\"\n        stroke=\"currentColor\"\n        strokeWidth=\"0.5\"\n      />\n    </pattern>\n  ),\n  plus: ({ id }) => (\n    <pattern id={id} x=\"0\" y=\"0\" width=\"16\" height=\"16\" patternUnits=\"userSpaceOnUse\">\n      <path\n        className=\"text-border\"\n        d=\"M 8 4 L 8 12 M 4 8 L 12 8\"\n        fill=\"none\"\n        stroke=\"currentColor\"\n        strokeWidth=\"0.5\"\n        strokeLinecap=\"round\"\n      />\n    </pattern>\n  ),\n  \"falling-triangles\": ({ id }) => (\n    <pattern id={id} x=\"0\" y=\"0\" width=\"18\" height=\"36\" patternUnits=\"userSpaceOnUse\">\n      <path\n        className=\"text-border\"\n        d=\"M2 6h12L8 18 2 6zm18 36h12l-6 12-6-12z\"\n        transform=\"scale(0.5)\"\n        fill=\"currentColor\"\n        fillOpacity=\"0.4\"\n      />\n    </pattern>\n  ),\n  \"4-pointed-star\": ({ id }) => (\n    <pattern id={id} x=\"0\" y=\"0\" width=\"16\" height=\"16\" patternUnits=\"userSpaceOnUse\">\n      <polygon\n        className=\"text-border\"\n        fillRule=\"evenodd\"\n        points=\"5 3 8 4 5 5 4 8 3 5 0 4 3 3 4 0 5 3\"\n        fill=\"currentColor\"\n        fillOpacity=\"0.4\"\n      />\n    </pattern>\n  ),\n  \"tiny-checkers\": ({ id }) => (\n    <pattern id={id} x=\"0\" y=\"0\" width=\"8\" height=\"8\" patternUnits=\"userSpaceOnUse\">\n      <path\n        className=\"text-border\"\n        fillRule=\"evenodd\"\n        d=\"M0 0h4v4H0V0zm4 4h4v4H4V4z\"\n        fill=\"currentColor\"\n        fillOpacity=\"0.2\"\n      />\n    </pattern>\n  ),\n  \"overlapping-circles\": ({ id }) => (\n    <pattern id={id} x=\"0\" y=\"0\" width=\"40\" height=\"40\" patternUnits=\"userSpaceOnUse\">\n      <path\n        className=\"text-border\"\n        fillRule=\"evenodd\"\n        d=\"M25 25c0-2.762 2.238-5 5-5s5 2.238 5 5-2.238 5-5 5c0 2.762-2.238 5-5 5s-5-2.238-5-5 2.238-5 5-5zM5 5c0-2.762 2.238-5 5-5s5 2.238 5 5-2.238 5-5 5c0 2.762-2.238 5-5 5S0 12.762 0 10s2.238-5 5-5zm5 4c2.209 0 4-1.791 4-4s-1.791-4-4-4-4 1.791-4 4 1.791 4 4 4zm20 20c2.209 0 4-1.791 4-4s-1.791-4-4-4-4 1.791-4 4 1.791 4 4 4z\"\n        fill=\"currentColor\"\n        fillOpacity=\"0.4\"\n      />\n    </pattern>\n  ),\n  \"wiggle-lines\": ({ id }) => (\n    <pattern\n      id={id}\n      x=\"0\"\n      y=\"0\"\n      width=\"52\"\n      height=\"26\"\n      patternUnits=\"userSpaceOnUse\"\n      patternTransform=\"scale(0.6)\"\n    >\n      <path\n        className=\"text-border\"\n        d=\"M10 10c0-2.21-1.79-4-4-4-3.314 0-6-2.686-6-6h2c0 2.21 1.79 4 4 4 3.314 0 6 2.686 6 6 0 2.21 1.79 4 4 4 3.314 0 6 2.686 6 6 0 2.21 1.79 4 4 4v2c-3.314 0-6-2.686-6-6 0-2.21-1.79-4-4-4-3.314 0-6-2.686-6-6zm25.464-1.95l8.486 8.486-1.414 1.414-8.486-8.486 1.414-1.414z\"\n        fill=\"currentColor\"\n        fillOpacity=\"0.4\"\n      />\n    </pattern>\n  ),\n  bubbles: ({ id }) => (\n    <pattern\n      id={id}\n      x=\"0\"\n      y=\"0\"\n      width=\"100\"\n      height=\"100\"\n      patternUnits=\"userSpaceOnUse\"\n      patternTransform=\"scale(0.6667)\"\n    >\n      <path\n        className=\"text-border\"\n        d=\"M11 18c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm48 25c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm-43-7c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm63 31c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM34 90c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm56-76c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM12 86c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm28-65c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm23-11c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-6 60c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm29 22c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zM32 63c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm57-13c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-9-21c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM60 91c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM35 41c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM12 60c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2z\"\n        fill=\"currentColor\"\n        fillOpacity=\"0.4\"\n        fillRule=\"evenodd\"\n      />\n    </pattern>\n  ),\n};\n\nfunction ChartBackground({ variant }: { variant: BackgroundVariant }) {\n  const baseId = useId().replace(/:/g, \"\");\n  const patternId = `${baseId}-bg-${variant}`;\n  const maskId = `${baseId}-bg-edge-fade`;\n  const filterId = `${baseId}-bg-blur`;\n  const Pattern = BACKGROUND_PATTERNS[variant];\n\n  return (\n    <svg\n      className=\"pointer-events-none absolute inset-0 z-0 h-full w-full\"\n      width=\"100%\"\n      height=\"100%\"\n      aria-hidden\n    >\n      <defs>\n        <Pattern id={patternId} />\n        {/* Gaussian blur + inset white rect → soft transparent edges. */}\n        <filter id={filterId}>\n          <feGaussianBlur stdDeviation=\"25\" />\n        </filter>\n        <mask id={maskId} maskUnits=\"userSpaceOnUse\">\n          <rect x=\"8%\" y=\"20%\" width=\"85%\" height=\"60%\" fill=\"white\" filter={`url(#${filterId})`} />\n        </mask>\n      </defs>\n      <rect width=\"100%\" height=\"100%\" fill={`url(#${patternId})`} mask={`url(#${maskId})`} />\n    </svg>\n  );\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Option builders — pure functions from a snapshot context to ECharts option\n// fragments. The component reads its refs ONCE per build into this context;\n// nothing below touches React state or the chart instance, so each fragment can\n// be reasoned about (and tested) in isolation.\n// ─────────────────────────────────────────────────────────────────────────────\n\ntype OptionBuildContext = {\n  categories: string[]; // bar names, in data order (inner → outer ring)\n  values: number[]; // bar values aligned with categories\n  config: ChartConfig;\n  radialBar: RadialBarSlot;\n  variant: RadialVariant;\n  innerRadius: number | string;\n  outerRadius: number | string;\n  angleMax: number;\n  selectedBar: string | null;\n  hasSelection: boolean;\n  tooltipSlot: TooltipSlot;\n  isLoading: boolean;\n  loadingData: () => number[];\n  resolved: ResolvedColors;\n};\n\n// The polar coordinate systems: concentric rings live between innerRadius and\n// outerRadius, centered per the arc variant. TWO identical systems are emitted —\n// index 0 carries the data rings, index 1 the background track, so neither can\n// eat into the other's bar band (see buildTrackSeries).\nfunction buildPolar(ctx: OptionBuildContext): PolarOption[] {\n  const geom = getVariantGeometry(ctx.variant);\n  const polar: PolarOption = {\n    center: geom.center,\n    radius: [ctx.innerRadius, ctx.outerRadius] as (number | string)[],\n  };\n  return [polar, { ...polar }];\n}\n\n// The VALUE axis: a bar's length is its arc sweep, so the value maps to the\n// angle. Fully hidden — the Recharts twin shows no angular ticks or gridlines.\nfunction buildAngleAxis(ctx: OptionBuildContext): AngleAxisOption[] {\n  const geom = getVariantGeometry(ctx.variant);\n  const axis: AngleAxisOption = {\n    type: \"value\",\n    min: 0,\n    max: ctx.angleMax,\n    startAngle: geom.startAngle,\n    endAngle: geom.endAngle,\n    clockwise: true,\n    show: false,\n    axisLine: { show: false },\n    axisTick: { show: false },\n    axisLabel: { show: false },\n    splitLine: { show: false },\n  };\n  // One per polar — an axis binds to its system through `polarIndex`.\n  return [\n    { ...axis, polarIndex: 0 },\n    { ...axis, polarIndex: TRACK_POLAR_INDEX },\n  ];\n}\n\n// The CATEGORY axis: one band per ring. Category index 0 sits innermost, which\n// matches the Recharts twin (first data row = innermost ring). Fully hidden.\nfunction buildRadiusAxis(ctx: OptionBuildContext): RadiusAxisOption[] {\n  const axis: RadiusAxisOption = {\n    type: \"category\",\n    data: ctx.categories,\n    show: false,\n    axisLine: { show: false },\n    axisTick: { show: false },\n    axisLabel: { show: false },\n    splitLine: { show: false },\n  };\n  // Identical bands on both polars, so a ring and its track land on the same radius.\n  return [\n    { ...axis, polarIndex: 0 },\n    { ...axis, polarIndex: TRACK_POLAR_INDEX },\n  ];\n}\n\n// The unfilled track behind each bar — a full-range ring drawn only when\n// `showBackground` is set. ECharts' native `showBackground` always spans a full\n// 360° ring even in the `semi` variant (createBackgroundShape hardcodes\n// startAngle 0 / endAngle 2π for tangential polar bars), so the track is a real\n// (silent) bar series at the axis max instead: it fills exactly the arc range.\n//\n// `polarIndex: 1` is load-bearing. Bar series on ONE polar share the radiusAxis\n// band, and ECharts hands out that band per stack id, first come first served:\n// `barWidth = min(remainedWidth, barWidth)` (layout/barPolar.js). Two series\n// each asking for the same barSize therefore split the band — the ring claims\n// its full width and the track is clamped to the leftover, rendering thinner\n// and hugging the ring's inner edge instead of underlaying it. `barGap: \"-100%\"`\n// does NOT fix this: it only overlaps their offsets, never their widths. Giving\n// the track its own (identical) polar means each series is alone in its band, so\n// both get the requested width and the same centered offset — concentric by\n// construction, in every variant and at any barSize.\nfunction buildTrackSeries(ctx: OptionBuildContext, loading: boolean): BarSeriesOption {\n  const { radialBar } = ctx;\n  const trackColor = withAlpha(ctx.resolved.tokens.mutedForeground, TRACK_OPACITY);\n  return {\n    id: loading ? LOADING_TRACK_ID : TRACK_SERIES_ID,\n    type: \"bar\",\n    coordinateSystem: \"polar\",\n    polarIndex: TRACK_POLAR_INDEX,\n    data: ctx.categories.map(() => ctx.angleMax),\n    barWidth: radialBar.barSize,\n    roundCap: radialBar.cornerRadius > 0,\n    silent: true,\n    // Static: the track is present from the first frame, only the data rings\n    // sweep in (Recharts parity — its background sectors don't animate).\n    animation: false,\n    emphasis: { disabled: true },\n    itemStyle: { color: trackColor },\n    z: 1,\n  };\n}\n\n// The data rings. Each datum carries its own diagonal color fill and selection dim.\nfunction buildBarSeries(ctx: OptionBuildContext): BarSeriesOption[] {\n  const { categories, values, radialBar, selectedBar, hasSelection, resolved } = ctx;\n\n  const data = categories.map((name, i) => {\n    const slots = resolved.series[name] ?? [FALLBACK_COLOR];\n    const isSelected = selectedBar === null || selectedBar === name;\n    // Selection only dims when the bar is actually clickable (Recharts twin).\n    const dimmed = radialBar.isClickable && hasSelection && !isSelected;\n\n    const itemStyle: BarItemStyle = {\n      color: barPaint(slots),\n      opacity: dimmed ? SELECTED_DIM_OPACITY : 1,\n    };\n\n    return { value: values[i] ?? 0, itemStyle };\n  });\n\n  const main: BarSeriesOption = {\n    id: MAIN_SERIES_ID,\n    type: \"bar\",\n    coordinateSystem: \"polar\",\n    // Sole series on polar 0 — the track rides its own polar (see buildTrackSeries).\n    polarIndex: 0,\n    data,\n    barWidth: radialBar.barSize,\n    roundCap: radialBar.cornerRadius > 0,\n    cursor: radialBar.isClickable ? \"pointer\" : \"default\",\n    // The radial twin has no hover-highlight, so bars don't emphasise on hover.\n    emphasis: { disabled: true },\n    z: 3, // above the track (z 1)\n  };\n\n  // Main first (index 0) so `showTip` can target it by a stable index; z keeps it\n  // above the track regardless of array order.\n  return [main, ...(radialBar.showBackground ? [buildTrackSeries(ctx, false)] : [])];\n}\n\n// Tooltip HTML builder, closed over the build context. `trigger: \"item\"` — each\n// ring is an item; hovering one shows that bar. Labels by name (hideLabel parity:\n// no separate header row).\nfunction buildTooltipOption(ctx: OptionBuildContext): TooltipComponentOption {\n  const { tooltipSlot, config, categories, isLoading } = ctx;\n\n  const formatter = (params: unknown): string => {\n    const p = (Array.isArray(params) ? params[0] : params) as {\n      dataIndex?: number;\n      value?: number | string;\n      seriesId?: string;\n    };\n    if (p == null || String(p.seriesId ?? \"\").startsWith(\"__\")) return \"\";\n\n    const index = typeof p.dataIndex === \"number\" ? p.dataIndex : 0;\n    const key = categories[index] ?? \"\";\n    const item = config[key];\n    const colorsCount = item ? getColorsCount(item) : 1;\n    const labelText = typeof item?.label === \"string\" ? item.label : key;\n    const value = typeof p.value === \"number\" ? p.value.toLocaleString() : String(p.value ?? \"\");\n\n    // Item-trigger tooltip: one ring per hover, with no separate header row\n    // (hideLabel parity), so the shared tooltipShell — which always renders a\n    // label div — is intentionally NOT used. The row shape matches the shared\n    // indicator + label + value, so tooltipRow/tooltipIndicatorHtml build it.\n    const row = tooltipRow({\n      indicatorHtml: tooltipIndicatorHtml(key, colorsCount),\n      labelText,\n      valueText: value,\n      dimmed: \"\",\n    });\n\n    return `<div class=\"grid min-w-32 items-start gap-1.5 border border-border/50 px-2.5 py-1.5 text-xs shadow-xl ${roundnessClass[tooltipSlot.roundness]} ${tooltipVariantClass[tooltipSlot.variant]}\">\n      <div class=\"grid gap-1.5\">${row}</div>\n    </div>`;\n  };\n\n  return {\n    show: tooltipSlot.present && !isLoading,\n    trigger: \"item\",\n    confine: true,\n    backgroundColor: \"transparent\",\n    borderWidth: 0,\n    padding: 0,\n    extraCssText: \"box-shadow:none;\",\n    // \"variable\" → undefined (ECharts default item anchoring, current behavior);\n    // \"fixed\" → pin near the top, tracking the pointer's X only.\n    position: resolveTooltipPosition(tooltipSlot.position),\n    formatter,\n  };\n}\n\n// Loading skeleton — a faint set of full rings (the always-visible track) with a\n// bright band CLIPPED to a sweeping window on top (the `__loading` rings). One\n// gray skeleton regardless of the real data, matching the area twin's approach.\nfunction buildLoadingOption(ctx: OptionBuildContext): EChartsOption {\n  const { tokens } = ctx.resolved;\n  const loadingCats = Array.from({ length: LOADING_BARS }, (_, i) => String(i));\n  const loadingCtx: OptionBuildContext = {\n    ...ctx,\n    categories: loadingCats,\n    angleMax: LOADING_MAX,\n  };\n\n  return {\n    animation: false,\n    polar: buildPolar(loadingCtx),\n    angleAxis: buildAngleAxis(loadingCtx),\n    radiusAxis: buildRadiusAxis(loadingCtx),\n    tooltip: { show: false },\n    series: [\n      buildTrackSeries(loadingCtx, true),\n      {\n        id: LOADING_SERIES_ID,\n        type: \"bar\",\n        coordinateSystem: \"polar\",\n        polarIndex: 0,\n        data: ctx.loadingData(),\n        barWidth: loadingCtx.radialBar.barSize,\n        roundCap: loadingCtx.radialBar.cornerRadius > 0,\n        silent: true,\n        emphasis: { disabled: true },\n        // Invisible until the first shimmer tick positions the clip window.\n        itemStyle: { color: withAlpha(tokens.foreground, 0) },\n        z: 2,\n      },\n    ],\n  };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Live imperative state — everything the ECharts event handlers, rAF loop, and\n// theme/resize repushes read or write OUTSIDE the React render cycle, grouped in\n// one ref-stable object so the whole imperative surface is visible at a glance.\n// None of it is render output, which is exactly why it is not React state.\n// ─────────────────────────────────────────────────────────────────────────────\n\ntype LiveState = {\n  resolved: ResolvedColors | null; // colors read off the live DOM — feeds builds and the shimmer loop\n  hasRevealed: boolean; // the intro sweep already played on this chart instance\n  loadingRows: number[] | null; // skeleton data, lazily rolled and re-rolled per shimmer sweep\n  categories: string[]; // bar names of the last build (click → name lookup)\n  valueByName: Map<string, number>; // bar values (legend/click → selection value)\n  handlers: { clickable: boolean }; // latest flags for the imperative click handler\n  // Update-style re-push for paths that bypass React entirely (theme flips,\n  // resizes) — set by the sync effect.\n  repush: () => void;\n};\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Component\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Apache ECharts port of the EvilCharts radial chart, exposing a\n * compound-as-config API so its JSX reads identically to the Recharts twin. The\n * root owns the data, config, selection state, loading skeleton, and intro\n * reveal; every visual part — `<RadialBar>`, `<Tooltip>`, `<Legend>` — is\n * composed as a declarative child that renders nothing. The root walks those\n * children by reference and drives a single imperative ECharts instance. Fully\n * self-contained: its only dependencies are `react`, `echarts`, and `motion`.\n */\nexport function EChartsRadialChart<TData extends Record<string, unknown>>({\n  data,\n  config,\n  nameKey,\n  className,\n  variant = \"full\",\n  max,\n  innerRadius = DEFAULT_INNER_RADIUS,\n  outerRadius = DEFAULT_OUTER_RADIUS,\n  defaultSelectedDataKey = null,\n  onSelectionChange,\n  isLoading = false,\n  backgroundVariant,\n  chartOptions,\n  children,\n}: EChartsRadialChartProps<TData>) {\n  const rawId = useId();\n  const chartId = `chart-${rawId.replace(/:/g, \"\")}`;\n\n  const containerRef = useRef<HTMLDivElement>(null);\n  const mountRef = useRef<HTMLDivElement>(null);\n  const echartsRef = useRef<EChartsInstance | null>(null);\n\n  // The single imperative surface (see LiveState). `resolved` lives here rather\n  // than in state: as state it would force an extra render pass and an effect\n  // whose only job is to trigger the option push — the \"chain of computations\"\n  // react.dev/learn/you-might-not-need-an-effect warns about. The object\n  // identity is stable for the component's lifetime.\n  const live = useRef<LiveState>({\n    resolved: null,\n    hasRevealed: false,\n    loadingRows: null,\n    categories: [],\n    valueByName: new Map<string, number>(),\n    handlers: { clickable: false },\n    repush: () => {},\n  }).current;\n\n  // Skeleton rows roll lazily on first use — an impure useRef initializer would\n  // re-roll Math.random() on every render.\n  const loadingData = useCallback(\n    () => (live.loadingRows ??= getLoadingData(LOADING_BARS)),\n    [live],\n  );\n  const shouldReduceMotion = useReducedMotion();\n\n  const [selectedBar, setSelectedBar] = useState<string | null>(defaultSelectedDataKey);\n\n  // ── Declarative config, collected from children by reference ─────────────────\n  const collected = useMemo(() => collectConfig(children), [children]);\n  const { radialBar, tooltip: tooltipSlot, legend: legendSlot } = collected;\n\n  const configKeys = useMemo(() => Object.keys(config), [config]);\n\n  // Bar names + values, in data order (index 0 → innermost ring).\n  const categories = useMemo(() => data.map((row) => String(row[nameKey])), [data, nameKey]);\n  const values = useMemo(\n    () => data.map((row) => Number(row[radialBar.dataKey]) || 0),\n    [data, radialBar.dataKey],\n  );\n\n  // An explicit `max` pins what a full sweep means (gauges). Otherwise a nice\n  // ceiling over the data so the largest ring stops just shy of a full wrap\n  // (Recharts derives an auto-scaled domain; the exact niced max differs slightly\n  // but keeps the same look).\n  const angleMax = useMemo(\n    () => (max != null && max > 0 ? max : niceCeil(Math.max(0, ...values))),\n    [max, values],\n  );\n\n  const css = useMemo(() => buildChartCss(chartId, config), [chartId, config]);\n  const hasSelection = selectedBar !== null;\n\n  // Refresh the imperative snapshots every render so the click handler and the\n  // legend/click selection see current values.\n  live.categories = categories;\n  live.handlers = { clickable: radialBar.isClickable };\n  live.valueByName = useMemo(() => {\n    const map = new Map<string, number>();\n    categories.forEach((name, i) => map.set(name, values[i] ?? 0));\n    return map;\n  }, [categories, values]);\n\n  // Toggle selection and notify the parent with the bar's value (or null on\n  // deselect). Selecting the active bar clears the selection.\n  const toggleSelection = useCallback(\n    (name: string) => {\n      setSelectedBar((prev) => {\n        const next = prev === name ? null : name;\n        onSelectionChange?.(\n          next === null ? null : { dataKey: next, value: live.valueByName.get(next) ?? 0 },\n        );\n        return next;\n      });\n    },\n    [onSelectionChange, live],\n  );\n\n  // ── Option builder ───────────────────────────────────────────────────────────\n  // Thin orchestrator over the pure builders: snapshot the resolved colors into\n  // an OptionBuildContext, then assemble.\n  const buildOption = useCallback((): EChartsOption => {\n    const resolved = live.resolved;\n    if (!resolved) return {};\n\n    const ctx: OptionBuildContext = {\n      categories,\n      values,\n      config,\n      radialBar,\n      variant,\n      innerRadius,\n      outerRadius,\n      angleMax,\n      selectedBar,\n      hasSelection,\n      tooltipSlot,\n      isLoading,\n      loadingData,\n      resolved,\n    };\n\n    if (isLoading) return buildLoadingOption(ctx);\n\n    return {\n      animation: false,\n      polar: buildPolar(ctx),\n      angleAxis: buildAngleAxis(ctx),\n      radiusAxis: buildRadiusAxis(ctx),\n      tooltip: buildTooltipOption(ctx),\n      series: buildBarSeries(ctx),\n    };\n  }, [\n    live,\n    categories,\n    values,\n    config,\n    radialBar,\n    variant,\n    innerRadius,\n    outerRadius,\n    angleMax,\n    selectedBar,\n    hasSelection,\n    tooltipSlot,\n    isLoading,\n    loadingData,\n  ]);\n\n  // ── Init + resize + theme observer (once) ────────────────────────────────────\n  useEffect(() => {\n    const mount = mountRef.current;\n    const container = containerRef.current;\n    if (!mount || !container) return;\n\n    const chart = echarts.init(mount);\n    echartsRef.current = chart;\n\n    const resizeObserver = new ResizeObserver(() => {\n      // Observers always fire once right after observe(). Repushing on that\n      // no-op fire would land one frame into the intro and stomp the reveal —\n      // only react when the renderer size actually changed.\n      if (mount.clientWidth === chart.getWidth() && mount.clientHeight === chart.getHeight()) {\n        return;\n      }\n      chart.resize();\n      live.repush();\n    });\n    resizeObserver.observe(mount);\n\n    // Light/dark flips change no React state — re-resolve and push directly.\n    const themeObserver = new MutationObserver(() => live.repush());\n    themeObserver.observe(document.documentElement, {\n      attributes: true,\n      attributeFilter: [\"class\"],\n    });\n\n    chart.on(\"click\", (params) => {\n      if (!live.handlers.clickable) return;\n      const p = params as { seriesId?: string; dataIndex?: number; componentType?: string };\n      // Only the main ring series is clickable; the track/skeleton are silent.\n      if (p.componentType !== \"series\" || p.seriesId !== MAIN_SERIES_ID) return;\n      if (typeof p.dataIndex !== \"number\") return;\n      const name = live.categories[p.dataIndex];\n      if (name != null) toggleSelection(name);\n    });\n\n    return () => {\n      resizeObserver.disconnect();\n      themeObserver.disconnect();\n      chart.dispose();\n      echartsRef.current = null;\n      // The reveal guard belongs to the chart instance it guarded. Without this\n      // reset, StrictMode's dev-only mount→unmount→remount plays the entrance on\n      // the throwaway instance and the surviving one renders without it.\n      live.hasRevealed = false;\n    };\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, []);\n\n  // ── Sync ECharts with props/theme/selection — resolve, build, push ────────────\n  useEffect(() => {\n    const chart = echartsRef.current;\n    const container = containerRef.current;\n    if (!chart || !container) return;\n\n    // Colors come from the <style> committed just before this effect ran — read\n    // them here, right before the push, rather than round-tripping through state.\n    live.resolved = resolveColors(container, config, configKeys);\n\n    const push = (withEntrance: boolean) => {\n      const option = buildOption();\n      const merged = chartOptions ? { ...option, ...chartOptions } : option;\n      Object.assign(merged, {\n        animation: withEntrance,\n        animationDuration: REVEAL_DURATION,\n        animationDurationUpdate: 0,\n      });\n      // chartOptions is an untyped escape hatch — the spread erases the option's\n      // shape, so re-assert it. The only cast in the file.\n      chart.setOption(merged as EChartsOption, { notMerge: true });\n\n      // Show the default tooltip once the option is in place (item trigger →\n      // target the main series by its stable index).\n      if (!isLoading && tooltipSlot.present && tooltipSlot.defaultIndex != null) {\n        chart.dispatchAction({\n          type: \"showTip\",\n          seriesIndex: 0,\n          dataIndex: tooltipSlot.defaultIndex,\n        });\n      }\n    };\n\n    // Intro reveal — ECharts' native bar sweep, enabled only for the first real\n    // render. Every later push (selection, theme) applies instantly, since\n    // notMerge would otherwise replay the entrance on each of them. A loading\n    // cycle re-arms it: the Recharts twin remounts its <RadialBar> while loading\n    // and replays the intro, so data → loading → data sweeps in again here too.\n    if (isLoading) live.hasRevealed = false;\n    const shouldReveal = !live.hasRevealed && !isLoading;\n    if (shouldReveal) live.hasRevealed = true;\n    const revealEnabled = shouldReveal && !shouldReduceMotion;\n    push(revealEnabled);\n\n    // Theme flips and resizes re-enter here without touching React: re-read the\n    // tokens (the .dark class changed) and push an update-style option.\n    live.repush = () => {\n      live.resolved = resolveColors(container, config, configKeys);\n      push(false);\n    };\n  }, [\n    live,\n    buildOption,\n    chartOptions,\n    isLoading,\n    shouldReduceMotion,\n    config,\n    configKeys,\n    tooltipSlot,\n  ]);\n\n  // ── Loading shimmer — rAF sweeps a bright band, regenerating data off-screen ──\n  useEffect(() => {\n    const chart = echartsRef.current;\n    if (!chart || !isLoading) return;\n\n    let raf = 0;\n    let lastPhase = 0;\n    const start = performance.now();\n    const tick = (now: number) => {\n      const phase = ((((now - start) / LOADING_ANIMATION_DURATION) % 1) + 1) % 1;\n      // Wrapped past 1 → the band is off-screen; swap in fresh random data.\n      if (phase < lastPhase) live.loadingRows = getLoadingData(LOADING_BARS);\n      lastPhase = phase;\n\n      // Read tokens per frame, so a theme flip mid-loading retints the shimmer.\n      const foreground = live.resolved?.tokens.foreground ?? FALLBACK_COLOR;\n      const w = chart.getWidth();\n      const h = chart.getHeight();\n      if (!w || !h) {\n        raf = requestAnimationFrame(tick);\n        return;\n      }\n      // Sweep the clip window from fully off-screen to fully off-screen along a\n      // 45° axis (ABSOLUTE pixel coords via the gradient's `global` flag), so\n      // every ring is cut by the same diagonal band.\n      const maxT = (w + h) / (2 * w);\n      const center = phase * (maxT + 2 * LOADING_SHIMMER_BAND) - LOADING_SHIMMER_BAND;\n      const clip = new echarts.graphic.LinearGradient(\n        0,\n        0,\n        w,\n        w,\n        shimmerWindowStops(center, foreground, LOADING_SHIMMER_MAX_OPACITY),\n        true,\n      );\n      chart.setOption(\n        { series: [{ id: LOADING_SERIES_ID, data: loadingData(), itemStyle: { color: clip } }] },\n        { silent: true, lazyUpdate: true },\n      );\n      raf = requestAnimationFrame(tick);\n    };\n    raf = requestAnimationFrame(tick);\n    return () => cancelAnimationFrame(raf);\n  }, [live, isLoading, loadingData]);\n\n  // ── Legend overlay (HTML) ─────────────────────────────────────────────────────\n  // One entry per ring. Unlike the area twin's absolutely-positioned legend, the\n  // top/bottom placements flow above/below the canvas so they RESERVE space —\n  // the polar plot shrinks to fit, matching how Recharts lays the legend out.\n  // `middle` overlays, centered.\n  const legendJustify =\n    legendSlot.align === \"left\"\n      ? \"justify-start\"\n      : legendSlot.align === \"center\"\n        ? \"justify-center\"\n        : \"justify-end\";\n\n  const renderLegend = (overlay: boolean) => (\n    <div\n      className={`flex flex-wrap items-center gap-3 px-4 select-none ${legendJustify} ${\n        overlay ? \"pointer-events-auto absolute inset-x-4 top-1/2 -translate-y-1/2\" : \"py-2\"\n      }`}\n    >\n      {categories.map((name) => {\n        const item = config[name];\n        const colorsCount = item ? getColorsCount(item) : 1;\n        const isActive = selectedBar === null || selectedBar === name;\n        return (\n          <div\n            key={name}\n            className={`flex items-center gap-1.5 transition-opacity ${\n              !isActive ? \"opacity-30\" : \"\"\n            } ${legendSlot.isClickable ? \"cursor-pointer\" : \"\"}`}\n            onClick={() => {\n              if (legendSlot.isClickable) toggleSelection(name);\n            }}\n          >\n            <LegendIndicator\n              variant={legendSlot.variant}\n              dataKey={name}\n              colorsCount={colorsCount}\n            />\n            {item?.label ?? name}\n          </div>\n        );\n      })}\n    </div>\n  );\n\n  const showLegend = legendSlot.present && !isLoading;\n  const legendTop = showLegend && legendSlot.verticalAlign === \"top\";\n  const legendBottom = showLegend && legendSlot.verticalAlign === \"bottom\";\n  const legendMiddle = showLegend && legendSlot.verticalAlign === \"middle\";\n\n  return (\n    <div\n      ref={containerRef}\n      data-chart={chartId}\n      className={`relative flex flex-col text-xs ${className ?? \"\"}`}\n    >\n      <style dangerouslySetInnerHTML={{ __html: css }} />\n\n      {legendTop && renderLegend(false)}\n\n      <div className=\"relative min-h-0 w-full flex-1\">\n        {backgroundVariant && <ChartBackground variant={backgroundVariant} />}\n        {/* Canvas is transparent, so the background SVG shows through behind it. */}\n        <div ref={mountRef} className=\"relative z-10 h-full min-h-0 w-full\" />\n        {legendMiddle && renderLegend(true)}\n      </div>\n\n      {legendBottom && renderLegend(false)}\n\n      {isLoading && (\n        <div className=\"pointer-events-none absolute inset-0 z-20 flex items-center justify-center\">\n          <motion.div\n            initial={shouldReduceMotion ? false : { opacity: 0, scale: 0.92 }}\n            animate={{ opacity: 1, scale: 1 }}\n            transition={{ duration: 0.25, ease: \"easeOut\" }}\n            className=\"text-primary bg-background flex items-center justify-center gap-2 rounded-md border px-2 py-0.5 text-sm\"\n          >\n            <div className=\"border-border border-t-primary h-3 w-3 animate-spin rounded-full border\" />\n            <span>Loading</span>\n          </motion.div>\n        </div>\n      )}\n    </div>\n  );\n}\n\nEChartsRadialChart.RadialBar = RadialBar;\nEChartsRadialChart.Tooltip = Tooltip;\nEChartsRadialChart.Legend = Legend;\n",
      "type": "registry:component",
      "target": "components/evilcharts/charts/echarts-radial-chart.tsx"
    }
  ],
  "type": "registry:component"
}