{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "echarts-pie-chart",
  "description": "Pie 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-pie-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  Children,\n  isValidElement,\n  useCallback,\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n  type CSSProperties,\n  type FC,\n  type ReactNode,\n} from \"react\";\nimport {\n  buildChartCss,\n  getColorsCount,\n  resolveColors,\n  withAlpha,\n  type ChartConfig,\n  type ResolvedColors,\n} from \"@/registry/ui/echarts-chart\";\nimport { TooltipComponent, type TooltipComponentOption } from \"echarts/components\";\nimport { LegendOverlay, type LegendVariant } from \"@/registry/ui/echarts-legend\";\nimport { PieChart, type PieSeriesOption } 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// A pie has no coordinate system, so there is no GridComponent and no axes; the\n// tooltip is item-triggered. Never register GraphicComponent: the loading shimmer\n// and selection dim are per-sector itemStyle updates, not graphic overlays.\necharts.use([PieChart, TooltipComponent, CanvasRenderer]);\n\ntype EChartsInstance = ReturnType<typeof echarts.init>;\n\n// The exact option surface this chart uses — a pie series plus the tooltip\n// component. Narrower than echarts' full EChartsOption, so a misspelled key fails\n// the compile instead of silently reaching setOption.\ntype EChartsOption = ComposeOption<PieSeriesOption | TooltipComponentOption>;\n\n// Sector paint — structurally assignable to a pie datum's itemStyle. `color`\n// accepts the same solid-or-gradient value sectorPaint returns, and the optional\n// border fields carry the constant-width gap / overlap separator.\ntype PieItemStyle = {\n  color: string | echarts.graphic.LinearGradient;\n  opacity: number;\n  borderRadius: number;\n  borderColor?: string;\n  borderWidth?: number;\n};\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Constants\n// ─────────────────────────────────────────────────────────────────────────────\n\nconst REVEAL_DURATION = 1000; // intro draw-in length, in milliseconds (ECharts' default)\n// NOTE: the intro is ECharts' RAW default pie entrance (`animationType: \"expansion\"`\n// — sectors sweep out from the start angle). We only gate whether it plays; we do\n// not customize its easing, matching the area chart's \"raw default\" policy.\nconst LOADING_ANIMATION_DURATION = 2000; // shimmer loop, in milliseconds\nconst LOADING_SECTORS = 5; // skeleton sector count — Recharts twin uses 5 equal sectors\n\nconst DEFAULT_INNER_RADIUS: number | string = 0;\nconst DEFAULT_OUTER_RADIUS: number | string = \"80%\";\nconst DEFAULT_CORNER_RADIUS = 0;\nconst DEFAULT_PADDING_ANGLE = 0;\nconst DEFAULT_START_ANGLE = 0;\nconst DEFAULT_END_ANGLE = 360;\n\nconst FALLBACK_COLOR = \"rgba(120, 120, 120, 1)\";\n\n// Overlapping sectors (negative paddingAngle) get a background-colored border to\n// separate the petals — the canvas analogue of the Recharts twin's\n// `stroke=\"var(--background)\" strokeWidth={5}`.\nconst OVERLAP_BORDER_WIDTH = 5;\n\n// Selecting a sector pops it radially OUTWARD from the center — the offset-slice\n// look from the official ECharts pie-pattern example. This is the pixel distance\n// the chosen sector translates along its own bisector; deselecting returns it.\nconst SELECTED_OFFSET = 12;\n\n// The selected sector stays fully opaque; the others recede to this dimmed\n// opacity. Tuned to ~half the former 0.3 so the selected sector reads with more\n// contrast against the dimmed ones — the analogue of the area chart's dim-fill\n// halving (its dimmed fill went 0.2 → 0.1 while the selected state stays full).\nconst DIMMED_OPACITY = 0.15;\n\n// Positive gaps between sectors are drawn as a CONSTANT-WIDTH background-colored\n// border (px), NOT an angular padAngle. An angular pad tapers to a wedge toward\n// the center; a border keeps every gap parallel-edged all the way from the rim to\n// the center. The px width tracks the requested `paddingAngle` for familiar sizing.\nfunction gapBorderWidth(paddingAngle: number): number {\n  return Math.max(paddingAngle, 0);\n}\n\n// Resolves each sector's separator border. Negative paddingAngle keeps the\n// overlapping-petal look (a real angular overlap plus a wide separator); positive\n// paddingAngle becomes a constant-width gap; zero draws no border at all.\nfunction sectorBorder(\n  paddingAngle: number,\n  background: string,\n): { borderColor: string; borderWidth: number } | null {\n  if (paddingAngle < 0) return { borderColor: background, borderWidth: OVERLAP_BORDER_WIDTH };\n  const width = gapBorderWidth(paddingAngle);\n  if (width > 0) return { borderColor: background, borderWidth: width };\n  return null;\n}\n\n// Loading shimmer opacities (× the foreground token's own alpha). A sine-feathered\n// window sweeps around the ring, brightening each sector from base → peak as it\n// passes — the angular twin of the area chart's swept clip window, and a match for\n// the Recharts twin's staggered sector pulse.\nconst LOADING_BASE_OPACITY = 0.15; // resting sector fill, × foreground alpha\nconst LOADING_PEAK_OPACITY = 0.5; // fill inside the sweep window, × foreground alpha\nconst LOADING_SHIMMER_BAND = 0.28; // window half-width, fraction of the ring (0..1)\nconst LOADING_SHIMMER_FEATHER = 0.22; // sine-eased edge softening of the window\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Public types\n// ─────────────────────────────────────────────────────────────────────────────\n\n// The pie has a single fill style — a per-sector color gradient. Kept as a named\n// union for API parity with the Recharts twin (and room to grow).\nexport type PieVariant = \"gradient\";\n// Where sector labels sit: \"inside\" draws value text on the sector (the default),\n// \"outside\" moves the sector's name past the rim with a leader line, matching the\n// classic ECharts pie (echarts.apache.org/examples/en/editor.html?c=pie-simple).\nexport type LabelPosition = \"inside\" | \"outside\";\n// TooltipVariant, TooltipRoundness, TooltipPosition, LegendVariant, and ChartConfig\n// now live in the shared @/registry/ui/echarts/* modules and are imported +\n// re-exported at the top of this file.\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\nexport interface EChartsPieChartProps<TData extends Record<string, unknown>> {\n  data: TData[]; // rows rendered by the chart — one sector each\n  config: ChartConfig; // sector colors + labels, keyed by the sector name\n  dataKey: keyof TData & string; // key holding each sector's numeric value\n  nameKey: keyof TData & string; // key holding each sector's name\n  className?: string; // extra classes for the chart container\n  // Master switch for the intro draw-in. Not present on the Recharts twin (which\n  // hardcodes its animation); added here as the canvas off-switch, mirroring the\n  // ECharts area chart's `animation` prop. OS reduce-motion also disables it.\n  animation?: boolean;\n  defaultSelectedSector?: string | null; // sector selected on first render\n  selectedSector?: string | null; // controlled selection — overrides internal state when set\n  onSelectionChange?: (selection: { dataKey: string; value: number } | null) => void; // fires when the selected sector changes\n  isLoading?: boolean; // shows the animated loading skeleton\n  chartOptions?: Record<string, unknown>; // escape hatch merged over the built ECharts option\n  children?: ReactNode; // declarative config — <Pie>, <Tooltip>, <Legend>, <Background>\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Composible parts — DECLARATIVE CONFIG. Every part renders `null`; the root\n// walks `children` by reference (child.type === Pie, …) to collect its props.\n// Presence semantics mirror the Recharts twin: omit a child and that part does\n// not render. These are never mounted into the tree — they only carry props.\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport interface PieProps {\n  variant?: PieVariant; // fill style for the pie's sectors\n  innerRadius?: number | string; // inner radius — set above 0 for a donut\n  outerRadius?: number | string; // outer radius of the pie\n  cornerRadius?: number; // border-radius of each sector in pixels\n  paddingAngle?: number; // gap between sectors in degrees — negative overlaps them\n  startAngle?: number; // angle the pie starts drawing from\n  endAngle?: number; // angle the pie stops drawing at\n  isClickable?: boolean; // lets sectors be selected by clicking them — the selected sector pops outward\n  children?: ReactNode; // optional <Label> composition for sector labels\n}\n\n/**\n * The pie series. Declares its own shape (radii, angles, corner rounding,\n * padding) and clickability. Renders nothing — the root reads these props to\n * build the ECharts pie. When clickable, the selected sector pops radially\n * outward. Compose a <Label> inside it to draw labels on each sector.\n */\nconst Pie: FC<PieProps> = () => null;\n\nexport interface LabelProps {\n  dataKey?: string; // data key for the label text — defaults to the pie's value key\n  position?: LabelPosition; // \"inside\" (value on the sector) or \"outside\" (name past the rim, with a leader line)\n}\n\n/** Declares per-sector labels for the enclosing <Pie>. Renders nothing. */\nconst Label: FC<LabelProps> = () => 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; // sector 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 sector\n}\n\n/** Presence enables the HTML legend overlay. Renders nothing. */\nconst Legend: FC<LegendProps> = () => null;\n\nexport interface BackgroundProps {\n  variant?: BackgroundVariant; // background pattern style\n}\n\n/** Presence draws a decorative SVG pattern behind the pie. Renders nothing. */\nconst Background: FC<BackgroundProps> = () => null;\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Children collection — walk the declarative config into plain objects the\n// option builder consumes. <Label> is read from the <Pie>'s own children; a\n// missing <Label> means sector labels do not render.\n// ─────────────────────────────────────────────────────────────────────────────\n\ntype PieSlot = {\n  variant: PieVariant;\n  innerRadius: number | string;\n  outerRadius: number | string;\n  cornerRadius: number;\n  paddingAngle: number;\n  startAngle: number;\n  endAngle: number;\n  isClickable: boolean;\n  labelDataKey: string | null; // null when no <Label> child is present\n  labelPosition: LabelPosition; // where the <Label> sits — only meaningful when labelDataKey !== null\n};\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};\ntype BackgroundSlot = { present: boolean; variant: BackgroundVariant };\n\ntype CollectedConfig = {\n  pie: PieSlot | null;\n  tooltip: TooltipSlot;\n  legend: LegendSlot;\n  background: BackgroundSlot;\n};\n\nfunction collectConfig(children: ReactNode): CollectedConfig {\n  let pie: PieSlot | null = null;\n  let tooltip: TooltipSlot = {\n    present: false,\n    variant: \"default\",\n    roundness: \"lg\",\n    position: \"variable\",\n  };\n  // Pie legend defaults differ from the area chart's: centered along the bottom.\n  let legend: LegendSlot = {\n    present: false,\n    variant: \"rounded-square\",\n    align: \"center\",\n    verticalAlign: \"bottom\",\n    isClickable: false,\n  };\n  let background: BackgroundSlot = { present: false, variant: \"dots\" };\n\n  Children.forEach(children, (child) => {\n    if (!isValidElement(child)) return;\n    const type = child.type;\n\n    if (type === Pie) {\n      const props = child.props as PieProps;\n      // A <Label> child (if any) declares per-sector labels.\n      let labelDataKey: string | null = null;\n      let labelPosition: LabelPosition = \"inside\";\n      Children.forEach(props.children, (labelChild) => {\n        if (!isValidElement(labelChild) || labelChild.type !== Label) return;\n        const labelProps = labelChild.props as LabelProps;\n        labelDataKey = labelProps.dataKey ?? \"\";\n        labelPosition = labelProps.position ?? \"inside\";\n      });\n      pie = {\n        variant: props.variant ?? \"gradient\",\n        innerRadius: props.innerRadius ?? DEFAULT_INNER_RADIUS,\n        outerRadius: props.outerRadius ?? DEFAULT_OUTER_RADIUS,\n        cornerRadius: props.cornerRadius ?? DEFAULT_CORNER_RADIUS,\n        paddingAngle: props.paddingAngle ?? DEFAULT_PADDING_ANGLE,\n        startAngle: props.startAngle ?? DEFAULT_START_ANGLE,\n        endAngle: props.endAngle ?? DEFAULT_END_ANGLE,\n        isClickable: props.isClickable ?? false,\n        labelDataKey,\n        labelPosition,\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    } else if (type === Background) {\n      const props = child.props as BackgroundProps;\n      background = { present: true, variant: props.variant ?? \"dots\" };\n    }\n  });\n\n  return { pie, tooltip, legend, background };\n}\n\n// Color plumbing (ChartConfig, getColorsCount, buildChartCss, withAlpha,\n// ResolvedColors, resolveColors) now lives in @/registry/ui/echarts-chart and is\n// imported at the top of this file. The pie keeps its own DIAGONAL sectorPaint\n// below (the shared seriesPaint is a horizontal gradient).\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Sector fill\n// ─────────────────────────────────────────────────────────────────────────────\n\n// Per-sector fill: a solid color for a single-color config, else a diagonal\n// top-left → bottom-right gradient across the sector's own bounding box. This\n// mirrors the Recharts twin's `RadialColorGradient` (a `linearGradient` with\n// x1/y1 = 0 and x2/y2 = 1). ECharts gradients are bbox-relative by default —\n// exactly what a per-sector gradient wants — so no `global` override is needed.\nfunction sectorPaint(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// Loading skeleton helpers\n// ─────────────────────────────────────────────────────────────────────────────\n\n// Per-sector shimmer alpha: a sine-feathered window centered on `center` (both\n// values are ring fractions in [0, 1)). Sectors inside the window read at\n// `LOADING_PEAK_OPACITY`, outside at `LOADING_BASE_OPACITY`, with a sine falloff\n// across the feather so the sweep edge isn't a hard cut. Distance wraps around\n// the ring — the highlight travels continuously, like the Recharts twin's pulse.\nfunction loadingSectorAlpha(pos: number, center: number): number {\n  const raw = Math.abs(pos - center);\n  const dist = Math.min(raw, 1 - raw); // shortest way around the ring\n  const half = LOADING_SHIMMER_BAND;\n  const feather = LOADING_SHIMMER_FEATHER;\n\n  if (dist >= half) return LOADING_BASE_OPACITY;\n  if (dist <= half - feather) return LOADING_PEAK_OPACITY;\n  // Sine-eased ramp — a linear one still reads as a hard cut.\n  const t = 1 - (dist - (half - feather)) / feather;\n  const eased = Math.sin((t * Math.PI) / 2);\n  return LOADING_BASE_OPACITY + (LOADING_PEAK_OPACITY - LOADING_BASE_OPACITY) * eased;\n}\n\n// Tooltip styling (roundnessClass, tooltipVariantClass, tooltipRow,\n// tooltipIndicatorHtml) and the legend overlay (LegendOverlay + its indicators)\n// now live in @/registry/ui/echarts/{tooltip,legend} and are imported at the top\n// of this file.\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Background overlay (SVG) — the Recharts twin renders its decorative pattern in\n// SVG, so we render the SAME SVG patterns as a layer BEHIND the transparent\n// ECharts canvas. Copied verbatim from the repo's <ChartBackground> so this file\n// depends on nothing outside `react`, `echarts`, and `motion`. The Tailwind\n// `text-border` classes resolve in the DOM; the blur-masked rect fades the edges.\n// ─────────────────────────────────────────────────────────────────────────────\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 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 BackgroundLayer({ 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 PatternComponent = BACKGROUND_PATTERNS[variant];\n\n  return (\n    <svg\n      className=\"pointer-events-none absolute inset-0 h-full w-full\"\n      aria-hidden\n      preserveAspectRatio=\"none\"\n    >\n      <defs>\n        <PatternComponent id={patternId} />\n        {/* Gaussian blur for a soft edge fade — a slightly inset white rect blurred\n            into a mask leaves smooth 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.\n// ─────────────────────────────────────────────────────────────────────────────\n\ntype OptionBuildContext = {\n  data: Record<string, unknown>[];\n  config: ChartConfig;\n  nameKey: string;\n  dataKey: string;\n  pie: PieSlot | null;\n  selectedSector: string | null;\n  tooltipSlot: TooltipSlot;\n  legendSlot: LegendSlot;\n  isLoading: boolean;\n  resolved: ResolvedColors;\n};\n\n// The pie's vertical centre reserves room for the HTML legend overlay: a legend\n// along the bottom nudges the pie up, one along the top nudges it down. Kept as a\n// percentage so it tracks the container size (the legend is fixed-height text).\nfunction pieCenterY(legendSlot: LegendSlot): string {\n  if (!legendSlot.present) return \"50%\";\n  if (legendSlot.verticalAlign === \"bottom\") return \"45%\";\n  if (legendSlot.verticalAlign === \"top\") return \"55%\";\n  return \"50%\";\n}\n\n// Tooltip HTML builder, closed over the build context. The pie tooltip is\n// item-triggered, so each hover surfaces exactly one sector — its indicator,\n// label, and value, matching ChartTooltipContent with `hideLabel` (no header).\nfunction createTooltipFormatter(ctx: OptionBuildContext) {\n  const { config, selectedSector, tooltipSlot } = ctx;\n\n  return (params: unknown): string => {\n    const p = (Array.isArray(params) ? params[0] : params) as {\n      name?: string;\n      value?: number | string;\n      seriesId?: string;\n    } | null;\n    // The loading skeleton is a `__`-prefixed series and never surfaces a tooltip.\n    if (!p || String(p.seriesId ?? \"\").startsWith(\"__\")) return \"\";\n\n    const name = String(p.name ?? \"\");\n    const item = config[name];\n    const colorsCount = item ? getColorsCount(item) : 1;\n    const labelText = typeof item?.label === \"string\" ? item.label : name;\n    const value = typeof p.value === \"number\" ? p.value.toLocaleString() : String(p.value ?? \"\");\n    const dimmed = selectedSector != null && selectedSector !== name ? \" opacity-30\" : \"\";\n\n    // The row shape matches the area chart's indicator + label + value, so it\n    // reuses the shared tooltipRow/tooltipIndicatorHtml. The pie tooltip is\n    // item-triggered with NO header (hideLabel), so it keeps its own no-header\n    // shell — built from the shared roundnessClass/tooltipVariantClass — rather\n    // than the header-carrying tooltipShell.\n    const row = tooltipRow({\n      indicatorHtml: tooltipIndicatorHtml(name, 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\nfunction buildTooltipOption(ctx: OptionBuildContext): TooltipComponentOption {\n  const { tooltipSlot, isLoading } = ctx;\n  // The pie tooltip is item-triggered (no axis, no cursor line), so it can't use\n  // the shared tooltipBaseOption (which is trigger:\"axis\" with an axisPointer).\n  // It keeps its own item-tooltip fields and wires the position prop directly\n  // through the shared resolveTooltipPosition — \"variable\" → undefined (default\n  // follow-the-pointer behavior), \"fixed\" → pinned near the top, tracking X.\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    displayTransition: false,\n    position: resolveTooltipPosition(tooltipSlot.position),\n    formatter: createTooltipFormatter(ctx),\n  };\n}\n\n// The pie series. Each row becomes a sector whose fill is its own color gradient,\n// dimmed when another sector is selected, and separated from its neighbors by a\n// constant-width background border (see sectorBorder). The selected sector pops\n// radially outward via ECharts' native select state (selectedOffset).\nfunction buildPieSeries(ctx: OptionBuildContext): PieSeriesOption[] {\n  const { data, config, nameKey, dataKey, pie, selectedSector, legendSlot, resolved } = ctx;\n  if (!pie) return [];\n  const { tokens } = resolved;\n  const hasSelection = selectedSector !== null;\n  // Selection (dim + pop-out) is only meaningful on a clickable pie.\n  const border = sectorBorder(pie.paddingAngle, tokens.background);\n\n  const sectors = data.map((row) => {\n    const name = String(row[nameKey]);\n    const slots = resolved.series[name] ?? [FALLBACK_COLOR];\n    // Only a clickable pie dims — a static one never has a selection to dim from.\n    const isSelected = pie.isClickable && selectedSector === name;\n    const isDimmed = pie.isClickable && hasSelection && selectedSector !== name;\n\n    const itemStyle: PieItemStyle = {\n      color: sectorPaint(slots),\n      opacity: isDimmed ? DIMMED_OPACITY : 1,\n      borderRadius: pie.cornerRadius,\n    };\n    // Constant-width background gap (positive paddingAngle) or overlap separator\n    // (negative). Parallel-edged from rim to center — no wedge-shaped taper.\n    if (border) {\n      itemStyle.borderColor = border.borderColor;\n      itemStyle.borderWidth = border.borderWidth;\n    }\n\n    // The `selected` flag drives the native offset — React selection is the single\n    // source of truth, re-applied on every notMerge push so it survives rebuilds.\n    return { name, value: Number(row[dataKey]) || 0, itemStyle, selected: isSelected };\n  });\n\n  const showLabel = pie.labelDataKey !== null;\n  const isOutside = pie.labelPosition === \"outside\";\n  // An explicit <Label dataKey> always wins. Otherwise inside labels show the\n  // sector's value (Recharts parity) and outside labels show the sector's name/\n  // config label — matching the classic ECharts pie-simple outer labels.\n  const explicitKey = pie.labelDataKey ? pie.labelDataKey : null;\n  const labelFormatter = (labelParams: { dataIndex: number; name?: string; value?: unknown }) => {\n    if (explicitKey) return String(data[labelParams.dataIndex]?.[explicitKey] ?? \"\");\n    if (isOutside) {\n      const item = config[String(labelParams.name ?? \"\")];\n      return typeof item?.label === \"string\" ? item.label : String(labelParams.name ?? \"\");\n    }\n    return String(data[labelParams.dataIndex]?.[dataKey] ?? labelParams.value ?? \"\");\n  };\n\n  const label = {\n    show: showLabel,\n    // Inner value labels sit on the colored sector (background-colored text);\n    // outer name labels sit past the rim in muted-foreground with a leader.\n    position: (isOutside ? \"outside\" : \"inner\") as \"outside\" | \"inner\",\n    color: isOutside ? tokens.mutedForeground : tokens.background,\n    fontSize: 12,\n    fontWeight: 500,\n    formatter: labelFormatter,\n  };\n\n  return [\n    {\n      id: \"pie\",\n      type: \"pie\",\n      center: [\"50%\", pieCenterY(legendSlot)],\n      radius: [pie.innerRadius, pie.outerRadius],\n      startAngle: pie.startAngle,\n      endAngle: pie.endAngle,\n      // Recharts sweeps counterclockwise from 3 o'clock; ECharts angles share that\n      // orientation, so `clockwise: false` reproduces the twin's sector order.\n      clockwise: false,\n      // Only a NEGATIVE paddingAngle reaches padAngle (petal overlap). Positive\n      // gaps are drawn as constant-width borders instead — an angular pad would\n      // taper to a wedge toward the center.\n      padAngle: Math.min(pie.paddingAngle, 0),\n      cursor: pie.isClickable ? \"pointer\" : \"default\",\n      // No hover scale on ANY variant — hovering only surfaces the tooltip. The\n      // pop-out below is the sole selection affordance, never a hover effect.\n      emphasis: { scale: false },\n      // Native select state: the chosen sector translates SELECTED_OFFSET px along\n      // its bisector, away from the center (the offset-slice pie-pattern look).\n      // Driven by each datum's `selected` flag; deselecting returns it.\n      selectedMode: pie.isClickable ? \"single\" : false,\n      selectedOffset: SELECTED_OFFSET,\n      // Neutralize any default select styling — the selected sector keeps its\n      // normal paint (inherited via state merge) and only its position moves.\n      select: { itemStyle: {} },\n      label,\n      labelLine: isOutside\n        ? {\n            show: true,\n            length: 14,\n            length2: 14,\n            smooth: false,\n            // Leader lines drawn in a muted token, matching the docs aesthetic.\n            lineStyle: { color: withAlpha(tokens.mutedForeground, 0.45), width: 1 },\n          }\n        : { show: false },\n      data: sectors,\n    },\n  ];\n}\n\n// Loading skeleton — ONE gray ring of equal sectors regardless of the real data\n// (Recharts parity), swept by the shimmer rAF. Respects the pie's shape so a\n// donut skeleton stays a donut. The per-sector color is a placeholder; the rAF\n// loop retints each sector every frame.\nfunction buildLoadingOption(ctx: OptionBuildContext): EChartsOption {\n  const { pie, legendSlot, resolved } = ctx;\n  const { tokens } = resolved;\n\n  const innerRadius = pie?.innerRadius ?? DEFAULT_INNER_RADIUS;\n  const outerRadius = pie?.outerRadius ?? DEFAULT_OUTER_RADIUS;\n  const cornerRadius = pie?.cornerRadius ?? DEFAULT_CORNER_RADIUS;\n  const paddingAngle = pie?.paddingAngle ?? DEFAULT_PADDING_ANGLE;\n  const startAngle = pie?.startAngle ?? DEFAULT_START_ANGLE;\n  const endAngle = pie?.endAngle ?? DEFAULT_END_ANGLE;\n\n  const border = sectorBorder(paddingAngle, tokens.background);\n  const sectors = Array.from({ length: LOADING_SECTORS }, (_, i) => {\n    const itemStyle: PieItemStyle = {\n      color: withAlpha(tokens.foreground, LOADING_BASE_OPACITY),\n      opacity: 1,\n      borderRadius: cornerRadius,\n    };\n    // Same constant-width gap / overlap separator as the real pie.\n    if (border) {\n      itemStyle.borderColor = border.borderColor;\n      itemStyle.borderWidth = border.borderWidth;\n    }\n    return { name: `__loading-${i}`, value: 1, itemStyle };\n  });\n\n  return {\n    animation: false,\n    tooltip: { show: false },\n    series: [\n      {\n        id: \"__loading\",\n        type: \"pie\",\n        center: [\"50%\", pieCenterY(legendSlot)],\n        radius: [innerRadius, outerRadius],\n        startAngle,\n        endAngle,\n        clockwise: false,\n        padAngle: Math.min(paddingAngle, 0),\n        silent: true,\n        emphasis: { scale: false },\n        label: { show: false },\n        labelLine: { show: false },\n        data: sectors,\n      },\n    ],\n  };\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Live imperative state — everything the ECharts event handlers and rAF loops\n// read or write OUTSIDE the React render cycle, grouped in one ref-stable object\n// so the whole imperative surface is visible at a glance. None of it is render\n// 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 rAF loops\n  hasRevealed: boolean; // the intro draw-in already played on this chart instance\n  // Latest callbacks/flags for the imperative ECharts click handler.\n  handlers: {\n    isClickable: boolean;\n    selectedSector: string | null;\n    selectSector: (name: string | null) => void;\n  };\n  // Update-style re-push for paths that bypass React entirely (theme flips) —\n  // set by the sync effect.\n  repush: () => void;\n};\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Component\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Apache ECharts port of the EvilCharts pie chart, exposing a compound-as-config\n * API so its JSX reads identically to the Recharts twin. The root owns the data,\n * config, selection state, loading skeleton, and intro reveal; every visual part —\n * `<Pie>`, `<Tooltip>`, `<Legend>`, `<Background>` — is composed as a declarative\n * child that renders nothing. The root walks those children by reference and\n * drives a single imperative ECharts instance. Fully self-contained: its only\n * dependencies are `react`, `echarts`, and `motion`.\n */\nexport function EChartsPieChart<TData extends Record<string, unknown>>({\n  data,\n  config,\n  dataKey,\n  nameKey,\n  className,\n  animation = true,\n  defaultSelectedSector = null,\n  selectedSector: selectedSectorProp,\n  onSelectionChange,\n  isLoading = false,\n  chartOptions,\n  children,\n}: EChartsPieChartProps<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 object identity is stable\n  // for the component's lifetime.\n  const live = useRef<LiveState>({\n    resolved: null,\n    hasRevealed: false,\n    handlers: {\n      isClickable: false,\n      selectedSector: defaultSelectedSector,\n      selectSector: () => {},\n    },\n    repush: () => {},\n  }).current;\n\n  const shouldReduceMotion = useReducedMotion();\n\n  // Selection is controlled when the `selectedSector` prop is provided; otherwise\n  // the internal state (seeded by defaultSelectedSector) drives it.\n  const [internalSelectedSector, setSelectedSector] = useState<string | null>(defaultSelectedSector);\n  const selectedSector =\n    selectedSectorProp !== undefined ? selectedSectorProp : internalSelectedSector;\n\n  // ── Declarative config, collected from children by reference ─────────────────\n  const collected = useMemo(() => collectConfig(children), [children]);\n  const { pie, tooltip: tooltipSlot, legend: legendSlot, background: backgroundSlot } = collected;\n\n  // Sector names in data order — the keys color resolution, the legend, and the\n  // click handler all agree on. Config is keyed by these same names.\n  const sectorKeys = useMemo(\n    () => data.map((row) => String(row[nameKey as string])),\n    [data, nameKey],\n  );\n\n  const css = useMemo(() => buildChartCss(chartId, config), [chartId, config]);\n\n  // Sets selection state and notifies the parent with the sector's value.\n  const selectSector = useCallback(\n    (name: string | null) => {\n      setSelectedSector(name);\n      if (name === null) {\n        onSelectionChange?.(null);\n        return;\n      }\n      const item = data.find((row) => String(row[nameKey as string]) === name);\n      onSelectionChange?.(\n        item ? { dataKey: name, value: Number(item[dataKey as string]) || 0 } : null,\n      );\n    },\n    [data, dataKey, nameKey, onSelectionChange],\n  );\n\n  // Refresh the click handler's snapshot of the latest callbacks/flags every render.\n  live.handlers = {\n    isClickable: pie?.isClickable ?? false,\n    selectedSector,\n    selectSector,\n  };\n\n  // ── Option builder ───────────────────────────────────────────────────────────\n  // Thin orchestrator over the pure builders above: snapshot the resolved colors\n  // into an OptionBuildContext, then assemble.\n  const buildOption = useCallback((): EChartsOption => {\n    const resolved = live.resolved;\n    if (!resolved) return {};\n\n    const ctx: OptionBuildContext = {\n      data,\n      config,\n      nameKey: nameKey as string,\n      dataKey: dataKey as string,\n      pie,\n      selectedSector,\n      tooltipSlot,\n      legendSlot,\n      isLoading,\n      resolved,\n    };\n\n    if (isLoading) return buildLoadingOption(ctx);\n\n    return {\n      animation: false,\n      tooltip: buildTooltipOption(ctx),\n      series: buildPieSeries(ctx),\n    };\n  }, [\n    live,\n    data,\n    config,\n    nameKey,\n    dataKey,\n    pie,\n    selectedSector,\n    tooltipSlot,\n    legendSlot,\n    isLoading,\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 no-op\n      // fire would land one frame into the intro and stomp the reveal — only\n      // react when the renderer size actually changed. The pie has no\n      // renderer-sized textures, so a plain resize() (which re-lays the\n      // percentage geometry) is all a size change needs.\n      if (mount.clientWidth === chart.getWidth() && mount.clientHeight === chart.getHeight()) {\n        return;\n      }\n      chart.resize();\n    });\n    resizeObserver.observe(mount);\n\n    // Light/dark flips change no React state — re-resolve and push directly.\n    const themeObserver = new MutationObserver(() => {\n      live.repush();\n    });\n    themeObserver.observe(document.documentElement, {\n      attributes: true,\n      attributeFilter: [\"class\"],\n    });\n\n    chart.on(\"click\", (params) => {\n      const { isClickable, selectedSector: selected, selectSector: select } = live.handlers;\n      if (!isClickable) return;\n      const p = params as { name?: string; seriesId?: string };\n      // Ignore the loading skeleton's `__`-prefixed series.\n      if (String(p.seriesId ?? \"\").startsWith(\"__\")) return;\n      const name = p.name;\n      if (typeof name !== \"string\") return;\n      // Clicking the selected sector clears the selection, otherwise selects it.\n      select(selected === name ? null : 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, sectorKeys);\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\n    // Intro reveal — ECharts' native pie expansion, enabled only for the first\n    // real render. Every later push (selection, theme) applies instantly, since\n    // notMerge would otherwise replay the entrance on each. A loading cycle\n    // re-arms it: the Recharts twin remounts its sectors after loading and\n    // replays the intro, so data → loading → data draws 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 = animation && shouldReveal && !shouldReduceMotion;\n    push(revealEnabled);\n\n    // Theme flips re-enter here without touching React: re-read the tokens (the\n    // .dark class changed) and push an update-style option.\n    live.repush = () => {\n      live.resolved = resolveColors(container, config, sectorKeys);\n      push(false);\n    };\n  }, [\n    live,\n    buildOption,\n    chartOptions,\n    isLoading,\n    animation,\n    shouldReduceMotion,\n    config,\n    sectorKeys,\n  ]);\n\n  // ── Default tooltip index — show a sector's tooltip with no hover ─────────────\n  useEffect(() => {\n    const chart = echartsRef.current;\n    if (!chart || isLoading || !tooltipSlot.present || tooltipSlot.defaultIndex == null) return;\n    // Dispatch after paint so the sector geometry exists to anchor the tooltip.\n    const raf = requestAnimationFrame(() => {\n      chart.dispatchAction({\n        type: \"showTip\",\n        seriesIndex: 0,\n        dataIndex: tooltipSlot.defaultIndex,\n      });\n    });\n    return () => cancelAnimationFrame(raf);\n  }, [isLoading, tooltipSlot.present, tooltipSlot.defaultIndex]);\n\n  // ── Loading shimmer — rAF sweeps a bright window around the ring ──────────────\n  useEffect(() => {\n    const chart = echartsRef.current;\n    if (!chart || !isLoading) return;\n\n    const cornerRadius = pie?.cornerRadius ?? DEFAULT_CORNER_RADIUS;\n    const paddingAngle = pie?.paddingAngle ?? DEFAULT_PADDING_ANGLE;\n\n    let raf = 0;\n    const start = performance.now();\n    const tick = (now: number) => {\n      const phase = ((((now - start) / LOADING_ANIMATION_DURATION) % 1) + 1) % 1;\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 background = live.resolved?.tokens.background ?? FALLBACK_COLOR;\n\n      // Rebuild the full itemStyle for each sector — setOption replaces a series'\n      // data array wholesale, so a partial datum would drop the border/rounding.\n      const border = sectorBorder(paddingAngle, background);\n      const sectors = Array.from({ length: LOADING_SECTORS }, (_, i) => {\n        const pos = (i + 0.5) / LOADING_SECTORS;\n        const itemStyle: PieItemStyle = {\n          color: withAlpha(foreground, loadingSectorAlpha(pos, phase)),\n          opacity: 1,\n          borderRadius: cornerRadius,\n        };\n        if (border) {\n          itemStyle.borderColor = border.borderColor;\n          itemStyle.borderWidth = border.borderWidth;\n        }\n        return { value: 1, itemStyle };\n      });\n\n      chart.setOption(\n        { series: [{ id: \"__loading\", data: sectors }] },\n        { silent: true, lazyUpdate: true },\n      );\n      raf = requestAnimationFrame(tick);\n    };\n    raf = requestAnimationFrame(tick);\n    return () => cancelAnimationFrame(raf);\n  }, [live, isLoading, pie]);\n\n  // ── Legend overlay position ──────────────────────────────────────────────────\n  const legendStyle: CSSProperties = {\n    position: \"absolute\",\n    left: 16,\n    right: 16,\n    pointerEvents: \"auto\",\n    ...(legendSlot.verticalAlign === \"top\"\n      ? { top: 12 }\n      : legendSlot.verticalAlign === \"bottom\"\n        ? { bottom: 12 }\n        : { top: \"50%\", transform: \"translateY(-50%)\" }),\n  };\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      <div className=\"relative min-h-0 w-full flex-1\">\n        {backgroundSlot.present && !isLoading && (\n          <BackgroundLayer variant={backgroundSlot.variant} />\n        )}\n        <div ref={mountRef} className=\"relative h-full min-h-0 w-full\" />\n      </div>\n\n      {legendSlot.present && !isLoading && (\n        <LegendOverlay\n          seriesKeys={sectorKeys}\n          config={config}\n          variant={legendSlot.variant}\n          align={legendSlot.align}\n          verticalAlign={legendSlot.verticalAlign}\n          selectedKey={selectedSector}\n          hoveredKey={null}\n          isClickable={legendSlot.isClickable}\n          onToggle={(key) => selectSector(selectedSector === key ? null : key)}\n          style={legendStyle}\n        />\n      )}\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\n// Compound API: every part hangs off the root as a static member, so a consumer\n// writes <EChartsPieChart.Pie/>, <EChartsPieChart.Tooltip/>, … from a single\n// import — no colliding named marker exports when several charts share one file.\nEChartsPieChart.Pie = Pie;\nEChartsPieChart.Label = Label;\nEChartsPieChart.Tooltip = Tooltip;\nEChartsPieChart.Legend = Legend;\nEChartsPieChart.Background = Background;\n",
      "type": "registry:component",
      "target": "components/evilcharts/charts/echarts-pie-chart.tsx"
    }
  ],
  "type": "registry:component"
}