{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "pie-chart",
  "type": "registry:component",
  "title": "Pie Chart",
  "description": "A composable pie chart with animations and customizable slices",
  "dependencies": [
    "@number-flow/react",
    "@visx/group@4.0.1-alpha.0",
    "@visx/responsive@4.0.1-alpha.0",
    "@visx/shape@4.0.1-alpha.0",
    "d3-shape",
    "motion"
  ],
  "registryDependencies": [
    "@bklit/chart-animation",
    "@bklit/utils"
  ],
  "files": [
    {
      "path": "src/charts/pie-chart.tsx",
      "content": "\"use client\";\n\nimport { Group } from \"@visx/group\";\nimport { ParentSize } from \"@visx/responsive\";\nimport { arc as arcGenerator } from \"@visx/shape\";\nimport { pie as d3Pie } from \"d3-shape\";\nimport type { Transition } from \"motion/react\";\nimport {\n  Children,\n  isValidElement,\n  memo,\n  type ReactElement,\n  type ReactNode,\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  defaultPieColors,\n  type PieArcData,\n  type PieContextValue,\n  type PieData,\n  PieProvider,\n} from \"./pie-context\";\n\n/** Default hover offset in pixels */\nexport const DEFAULT_HOVER_OFFSET = 10;\n\nexport interface PieChartProps {\n  /** Data array - each item represents a slice */\n  data: PieData[];\n  /** Chart size in pixels. If not provided, uses parent container size */\n  size?: number;\n  /** Inner radius for donut charts. Default: 0 (solid pie) */\n  innerRadius?: number;\n  /** Padding angle between slices in radians. Default: 0 */\n  padAngle?: number;\n  /** Corner radius for rounded slice edges. Default: 0 */\n  cornerRadius?: number;\n  /** Start angle in radians. Default: -PI/2 (top) */\n  startAngle?: number;\n  /** End angle in radians. Default: 3*PI/2 (full circle from top) */\n  endAngle?: number;\n  /** Additional class name for the container */\n  className?: string;\n  /** Controlled hover state - index of hovered slice */\n  hoveredIndex?: number | null;\n  /** Callback when hover state changes */\n  onHoverChange?: (index: number | null) => void;\n  /**\n   * Hover offset in pixels for slice hover effects.\n   * This also determines the padding around the chart to prevent clipping.\n   * Default: 10\n   */\n  hoverOffset?: number;\n  /** Child components (PieSlice, PieCenter, patterns, gradients, etc.) */\n  children: ReactNode;\n  /** Framer Motion transition for slice enter animation */\n  enterTransition?: Transition;\n  /** Scales slice stagger delays (1 = default). */\n  enterStaggerScale?: number;\n  /**\n   * High-frequency geometry updates (e.g. studio NumberField scrub).\n   * Uses plain SVG paths instead of Motion `d` / spring hover morphing.\n   */\n  geometryScrubbing?: boolean;\n}\n\ninterface PieChartInnerProps {\n  width: number;\n  height: number;\n  data: PieData[];\n  innerRadius: number;\n  padAngle: number;\n  cornerRadius: number;\n  startAngle: number;\n  endAngle: number;\n  hoverOffset: number;\n  children: ReactNode;\n  containerRef: React.RefObject<HTMLDivElement | null>;\n  hoveredIndexProp?: number | null;\n  onHoverChange?: (index: number | null) => void;\n  enterTransition?: Transition;\n  enterStaggerScale: number;\n  geometryScrubbing: boolean;\n}\n\nfunction generatePieArcPath(\n  innerRadius: number,\n  outerRadius: number,\n  startAngle: number,\n  endAngle: number,\n  cornerRadius: number,\n  padAngle: number\n): string {\n  const generator = arcGenerator<unknown>({\n    innerRadius,\n    outerRadius,\n    cornerRadius,\n    padAngle,\n  });\n  return generator({ startAngle, endAngle } as unknown as null) || \"\";\n}\n\n// Helper to check if a child is a PieCenter component\nfunction isPieCenter(child: ReactNode): boolean {\n  return (\n    isValidElement(child) &&\n    typeof child.type === \"function\" &&\n    ((child.type as { displayName?: string }).displayName === \"PieCenter\" ||\n      (child.type as { name?: string }).name === \"PieCenter\")\n  );\n}\n\nfunction isPieSlice(child: ReactNode): boolean {\n  return (\n    isValidElement(child) &&\n    typeof child.type === \"function\" &&\n    ((child.type as { displayName?: string }).displayName === \"PieSlice\" ||\n      (child.type as { name?: string }).name === \"PieSlice\")\n  );\n}\n\n// Helper to check if a component is a gradient or pattern definition\nfunction isDefsComponent(child: ReactElement): boolean {\n  const displayName =\n    (child.type as { displayName?: string })?.displayName ||\n    (child.type as { name?: string })?.name ||\n    \"\";\n  return (\n    displayName.includes(\"Gradient\") ||\n    displayName.includes(\"Pattern\") ||\n    displayName === \"LinearGradient\" ||\n    displayName === \"RadialGradient\"\n  );\n}\n\nfunction PieChartInner(props: PieChartInnerProps) {\n  const size = Math.min(props.width, props.height);\n\n  if (size < 10) {\n    return null;\n  }\n\n  return <PieChartCore {...props} />;\n}\n\nconst PieChartCore = memo(function PieChartCore({\n  width,\n  height,\n  data,\n  innerRadius: innerRadiusProp,\n  padAngle,\n  cornerRadius,\n  startAngle,\n  endAngle,\n  hoverOffset,\n  children,\n  containerRef,\n  hoveredIndexProp,\n  onHoverChange,\n  enterTransition,\n  enterStaggerScale,\n  geometryScrubbing,\n}: PieChartInnerProps) {\n  const [internalHoveredIndex, setInternalHoveredIndex] = useState<\n    number | null\n  >(null);\n  const [animationKey] = useState(0);\n  const [isLoaded, setIsLoaded] = useState(false);\n\n  // Use controlled or uncontrolled hover state\n  const isControlled = hoveredIndexProp !== undefined;\n  const hoveredIndex = isControlled ? hoveredIndexProp : internalHoveredIndex;\n  const setHoveredIndex = useCallback(\n    (index: number | null) => {\n      if (isControlled) {\n        onHoverChange?.(index);\n      } else {\n        setInternalHoveredIndex(index);\n      }\n    },\n    [isControlled, onHoverChange]\n  );\n\n  // Use the smaller dimension to ensure the chart fits\n  const size = Math.min(width, height);\n  const center = size / 2;\n\n  // Calculate radii with padding based on hover offset to prevent clipping\n  const padding = hoverOffset;\n  const outerRadius = center - padding;\n  const innerRadius = innerRadiusProp;\n\n  // Calculate total value\n  const totalValue = useMemo(\n    () => data.reduce((sum, d) => sum + d.value, 0),\n    [data]\n  );\n\n  // Get color for a slice index\n  const getColor = useCallback(\n    (index: number) => {\n      const item = data[index];\n      if (item?.color) {\n        return item.color;\n      }\n      return defaultPieColors[index % defaultPieColors.length] as string;\n    },\n    [data]\n  );\n\n  // Get fill for a slice index (supports patterns/gradients)\n  const getFill = useCallback(\n    (index: number) => {\n      const item = data[index];\n      // Check for explicit fill (pattern/gradient URL)\n      if (item?.fill) {\n        return item.fill;\n      }\n      // Fall back to color\n      return getColor(index);\n    },\n    [data, getColor]\n  );\n\n  // Compute arcs using d3-shape pie\n  const arcs = useMemo(() => {\n    const pieGenerator = d3Pie<PieData>()\n      .value((d) => d.value)\n      .startAngle(startAngle)\n      .endAngle(endAngle)\n      .padAngle(padAngle)\n      .sort(null); // Maintain data order\n\n    const computed = pieGenerator(data);\n\n    return computed.map((arc, index) => ({\n      data: arc.data,\n      index,\n      startAngle: arc.startAngle,\n      endAngle: arc.endAngle,\n      padAngle: arc.padAngle,\n      value: arc.value,\n    })) as PieArcData[];\n  }, [data, startAngle, endAngle, padAngle]);\n\n  const scrubSlicePaths = useMemo((): readonly string[] | null => {\n    if (!geometryScrubbing) {\n      return null;\n    }\n    return arcs.map((arc) =>\n      generatePieArcPath(\n        innerRadius,\n        outerRadius,\n        arc.startAngle,\n        arc.endAngle,\n        cornerRadius,\n        arc.padAngle\n      )\n    );\n  }, [geometryScrubbing, arcs, innerRadius, outerRadius, cornerRadius]);\n\n  const effectiveIsLoaded = geometryScrubbing || isLoaded;\n\n  // biome-ignore lint/correctness/useExhaustiveDependencies: enterTransition\n  useEffect(() => {\n    if (geometryScrubbing) {\n      return;\n    }\n    setIsLoaded(false);\n    const timer = setTimeout(() => {\n      setIsLoaded(true);\n    }, 100);\n    return () => clearTimeout(timer);\n  }, [enterTransition, enterStaggerScale, geometryScrubbing]);\n\n  // Separate children into categories\n  const { svgChildren, centerChildren, defsChildren } = useMemo(() => {\n    const svgNodes: ReactNode[] = [];\n    const centerNodes: ReactNode[] = [];\n    const defsNodes: ReactElement[] = [];\n\n    Children.forEach(children, (child) => {\n      if (!isValidElement(child)) {\n        svgNodes.push(child);\n        return;\n      }\n\n      if (isPieCenter(child)) {\n        centerNodes.push(child);\n      } else if (isDefsComponent(child)) {\n        defsNodes.push(child);\n      } else if (geometryScrubbing && isPieSlice(child)) {\n        return;\n      } else {\n        svgNodes.push(child);\n      }\n    });\n\n    return {\n      svgChildren: svgNodes,\n      centerChildren: centerNodes,\n      defsChildren: defsNodes,\n    };\n  }, [children, geometryScrubbing]);\n\n  const scrubSliceFills = useMemo(() => {\n    if (!(geometryScrubbing && scrubSlicePaths)) {\n      return null;\n    }\n    return scrubSlicePaths.map((_, index) => getFill(index));\n  }, [geometryScrubbing, scrubSlicePaths, getFill]);\n\n  const contextValue: PieContextValue = useMemo(\n    () => ({\n      data,\n      arcs,\n      size,\n      center,\n      outerRadius,\n      innerRadius,\n      padAngle,\n      cornerRadius,\n      hoverOffset,\n      hoveredIndex,\n      setHoveredIndex,\n      animationKey,\n      isLoaded: effectiveIsLoaded,\n      enterTransition,\n      enterStaggerScale,\n      containerRef,\n      totalValue,\n      getColor,\n      getFill,\n      geometryScrubbing,\n      scrubSlicePaths,\n    }),\n    [\n      data,\n      arcs,\n      size,\n      center,\n      outerRadius,\n      innerRadius,\n      padAngle,\n      cornerRadius,\n      hoverOffset,\n      hoveredIndex,\n      setHoveredIndex,\n      animationKey,\n      effectiveIsLoaded,\n      enterTransition,\n      enterStaggerScale,\n      containerRef,\n      totalValue,\n      getColor,\n      getFill,\n      geometryScrubbing,\n      scrubSlicePaths,\n    ]\n  );\n\n  // Use CSS Grid stacking to layer SVG and HTML content\n  // This avoids Safari's foreignObject rendering bugs\n  return (\n    <PieProvider value={contextValue}>\n      <div\n        className=\"grid\"\n        style={{\n          gridTemplateColumns: \"1fr\",\n          gridTemplateRows: \"1fr\",\n          width: size,\n          height: size,\n        }}\n      >\n        {/* SVG layer with pie slices */}\n        <svg\n          aria-hidden=\"true\"\n          height={size}\n          style={{ gridArea: \"1 / 1\", contain: \"layout style paint\" }}\n          width={size}\n        >\n          {/* Defs for patterns and gradients */}\n          {defsChildren.length > 0 && <defs>{defsChildren}</defs>}\n\n          <Group left={center} top={center}>\n            {scrubSlicePaths && scrubSliceFills\n              ? scrubSlicePaths.map((d, index) =>\n                  d ? (\n                    <path\n                      d={d}\n                      fill={scrubSliceFills[index]}\n                      key={data[index]?.label ?? index}\n                      pointerEvents=\"none\"\n                    />\n                  ) : null\n                )\n              : null}\n            {svgChildren}\n          </Group>\n        </svg>\n\n        {/* HTML layer with center content - stacked on top via grid */}\n        {centerChildren.length > 0 && (\n          <div\n            className=\"pointer-events-none flex items-center justify-center\"\n            style={{ gridArea: \"1 / 1\" }}\n          >\n            {centerChildren}\n          </div>\n        )}\n      </div>\n    </PieProvider>\n  );\n}, pieChartCorePropsEqual);\n\nfunction pieChartCorePropsEqual(\n  prev: PieChartInnerProps,\n  next: PieChartInnerProps\n): boolean {\n  return (\n    prev.width === next.width &&\n    prev.height === next.height &&\n    prev.data === next.data &&\n    prev.innerRadius === next.innerRadius &&\n    prev.padAngle === next.padAngle &&\n    prev.cornerRadius === next.cornerRadius &&\n    prev.startAngle === next.startAngle &&\n    prev.endAngle === next.endAngle &&\n    prev.hoverOffset === next.hoverOffset &&\n    prev.hoveredIndexProp === next.hoveredIndexProp &&\n    prev.onHoverChange === next.onHoverChange &&\n    prev.enterTransition === next.enterTransition &&\n    prev.enterStaggerScale === next.enterStaggerScale &&\n    prev.geometryScrubbing === next.geometryScrubbing &&\n    prev.children === next.children\n  );\n}\n\nexport function PieChart({\n  data,\n  size: fixedSize,\n  innerRadius = 0,\n  padAngle = 0,\n  cornerRadius = 0,\n  startAngle = -Math.PI / 2,\n  endAngle = (3 * Math.PI) / 2,\n  className = \"\",\n  hoveredIndex,\n  onHoverChange,\n  hoverOffset = DEFAULT_HOVER_OFFSET,\n  enterTransition,\n  enterStaggerScale = 1,\n  geometryScrubbing = false,\n  children,\n}: PieChartProps) {\n  const containerRef = useRef<HTMLDivElement>(null);\n\n  // If fixed size is provided, use it directly\n  if (fixedSize) {\n    return (\n      <div\n        className={cn(\"relative flex items-center justify-center\", className)}\n        ref={containerRef}\n        style={{ width: fixedSize, height: fixedSize }}\n      >\n        <PieChartInner\n          containerRef={containerRef}\n          cornerRadius={cornerRadius}\n          data={data}\n          endAngle={endAngle}\n          enterStaggerScale={enterStaggerScale}\n          enterTransition={enterTransition}\n          geometryScrubbing={geometryScrubbing}\n          height={fixedSize}\n          hoveredIndexProp={hoveredIndex}\n          hoverOffset={hoverOffset}\n          innerRadius={innerRadius}\n          onHoverChange={onHoverChange}\n          padAngle={padAngle}\n          startAngle={startAngle}\n          width={fixedSize}\n        >\n          {children}\n        </PieChartInner>\n      </div>\n    );\n  }\n\n  // Otherwise use ParentSize for responsive sizing\n  return (\n    <div\n      className={cn(\"relative aspect-square w-full\", className)}\n      ref={containerRef}\n    >\n      <ParentSize debounceTime={10}>\n        {({ width, height }) => (\n          <PieChartInner\n            containerRef={containerRef}\n            cornerRadius={cornerRadius}\n            data={data}\n            endAngle={endAngle}\n            enterStaggerScale={enterStaggerScale}\n            enterTransition={enterTransition}\n            geometryScrubbing={geometryScrubbing}\n            height={height}\n            hoveredIndexProp={hoveredIndex}\n            hoverOffset={hoverOffset}\n            innerRadius={innerRadius}\n            onHoverChange={onHoverChange}\n            padAngle={padAngle}\n            startAngle={startAngle}\n            width={width}\n          >\n            {children}\n          </PieChartInner>\n        )}\n      </ParentSize>\n    </div>\n  );\n}\n\nPieChart.displayName = \"PieChart\";\n\nexport default PieChart;\n",
      "type": "registry:component",
      "target": "components/charts/pie-chart.tsx"
    },
    {
      "path": "src/charts/pie-context.tsx",
      "content": "\"use client\";\n\nimport type { Transition } from \"motion/react\";\nimport {\n  createContext,\n  type ReactNode,\n  type RefObject,\n  useContext,\n  useMemo,\n} from \"react\";\n\n// CSS variable references for pie chart theming\nexport const pieCssVars = {\n  background: \"var(--chart-background)\",\n  foreground: \"var(--chart-foreground)\",\n  foregroundMuted: \"var(--chart-foreground-muted)\",\n  label: \"var(--chart-label)\",\n  // Default slice colors from chart palette\n  slice1: \"var(--chart-1)\",\n  slice2: \"var(--chart-2)\",\n  slice3: \"var(--chart-3)\",\n  slice4: \"var(--chart-4)\",\n  slice5: \"var(--chart-5)\",\n};\n\n// Default slice color palette\nexport const defaultPieColors = [\n  pieCssVars.slice1,\n  pieCssVars.slice2,\n  pieCssVars.slice3,\n  pieCssVars.slice4,\n  pieCssVars.slice5,\n];\n\nexport interface PieData {\n  /** Display label for the slice */\n  label: string;\n  /** Value for the slice (determines slice size relative to total) */\n  value: number;\n  /** Optional color override - falls back to palette */\n  color?: string;\n  /** Optional fill override for patterns/gradients (e.g., \"url(#patternId)\") */\n  fill?: string;\n}\n\n/** Arc data computed by visx Pie */\nexport interface PieArcData {\n  data: PieData;\n  index: number;\n  startAngle: number;\n  endAngle: number;\n  padAngle: number;\n  value: number;\n}\n\nexport interface PieHoverContextValue {\n  hoveredIndex: number | null;\n  setHoveredIndex: (index: number | null) => void;\n}\n\nexport interface PieStableContextValue {\n  // Data\n  data: PieData[];\n  arcs: PieArcData[];\n\n  // Dimensions\n  size: number;\n  center: number;\n  outerRadius: number;\n  innerRadius: number;\n  padAngle: number;\n  cornerRadius: number;\n\n  // Hover effect\n  hoverOffset: number;\n\n  // Animation state\n  animationKey: number;\n  isLoaded: boolean;\n  enterTransition?: Transition;\n  enterStaggerScale: number;\n\n  // Container ref for portals\n  containerRef: RefObject<HTMLDivElement | null>;\n\n  // Computed values\n  totalValue: number;\n\n  // Get color for a slice index\n  getColor: (index: number) => string;\n\n  // Get fill for a slice index (supports patterns/gradients)\n  getFill: (index: number) => string;\n\n  /**\n   * Studio geometry scrub — skip Motion path morphing and use plain SVG paths.\n   * @default false\n   */\n  geometryScrubbing: boolean;\n\n  /** Precomputed slice paths during geometry scrub (one per arc). */\n  scrubSlicePaths: readonly string[] | null;\n}\n\nexport type PieContextValue = PieStableContextValue & PieHoverContextValue;\n\nconst PieStableContext = createContext<PieStableContextValue | null>(null);\nconst PieHoverContext = createContext<PieHoverContextValue | null>(null);\n\nexport function PieProvider({\n  children,\n  value,\n}: {\n  children: ReactNode;\n  value: PieContextValue;\n}) {\n  const stable = useMemo<PieStableContextValue>(\n    () => ({\n      data: value.data,\n      arcs: value.arcs,\n      size: value.size,\n      center: value.center,\n      outerRadius: value.outerRadius,\n      innerRadius: value.innerRadius,\n      padAngle: value.padAngle,\n      cornerRadius: value.cornerRadius,\n      hoverOffset: value.hoverOffset,\n      animationKey: value.animationKey,\n      isLoaded: value.isLoaded,\n      enterTransition: value.enterTransition,\n      enterStaggerScale: value.enterStaggerScale,\n      containerRef: value.containerRef,\n      totalValue: value.totalValue,\n      getColor: value.getColor,\n      getFill: value.getFill,\n      geometryScrubbing: value.geometryScrubbing,\n      scrubSlicePaths: value.scrubSlicePaths,\n    }),\n    [\n      value.data,\n      value.arcs,\n      value.size,\n      value.center,\n      value.outerRadius,\n      value.innerRadius,\n      value.padAngle,\n      value.cornerRadius,\n      value.hoverOffset,\n      value.animationKey,\n      value.isLoaded,\n      value.enterTransition,\n      value.enterStaggerScale,\n      value.containerRef,\n      value.totalValue,\n      value.getColor,\n      value.getFill,\n      value.geometryScrubbing,\n      value.scrubSlicePaths,\n    ]\n  );\n\n  const hover = useMemo<PieHoverContextValue>(\n    () => ({\n      hoveredIndex: value.hoveredIndex,\n      setHoveredIndex: value.setHoveredIndex,\n    }),\n    [value.hoveredIndex, value.setHoveredIndex]\n  );\n\n  return (\n    <PieStableContext.Provider value={stable}>\n      <PieHoverContext.Provider value={hover}>\n        {children}\n      </PieHoverContext.Provider>\n    </PieStableContext.Provider>\n  );\n}\n\nexport function usePieStable(): PieStableContextValue {\n  const context = useContext(PieStableContext);\n  if (!context) {\n    throw new Error(\n      \"usePieStable must be used within a PieProvider. \" +\n        \"Make sure your component is wrapped in <PieChart>.\"\n    );\n  }\n  return context;\n}\n\nexport function usePieHover(): PieHoverContextValue {\n  const context = useContext(PieHoverContext);\n  if (!context) {\n    throw new Error(\n      \"usePieHover must be used within a PieProvider. \" +\n        \"Make sure your component is wrapped in <PieChart>.\"\n    );\n  }\n  return context;\n}\n\nexport function usePie(): PieContextValue {\n  return { ...usePieStable(), ...usePieHover() };\n}\n\nexport default PieStableContext;\n",
      "type": "registry:component",
      "target": "components/charts/pie-context.tsx"
    },
    {
      "path": "src/charts/pie-slice.tsx",
      "content": "\"use client\";\n\nimport { arc as arcGenerator } from \"@visx/shape\";\nimport { motion, useSpring, useTransform } from \"motion/react\";\nimport { memo, useEffect } from \"react\";\nimport { usePieHover, usePieStable } from \"./pie-context\";\nimport { useEnterComplete } from \"./use-enter-complete\";\nimport { useMountProgress } from \"./use-mount-progress\";\n\n// Helper to generate arc path using d3 arc generator\nfunction generateArcPath(\n  innerRadius: number,\n  outerRadius: number,\n  startAngle: number,\n  endAngle: number,\n  cornerRadius: number,\n  padAngle: number\n): string {\n  const generator = arcGenerator<unknown>({\n    innerRadius,\n    outerRadius,\n    cornerRadius,\n    padAngle,\n  });\n  return generator({ startAngle, endAngle } as unknown as null) || \"\";\n}\n\n// Calculate the translation offset for a slice to \"pop out\" along its radial axis\nfunction getSliceOffset(\n  startAngle: number,\n  endAngle: number,\n  distance: number\n): { x: number; y: number } {\n  // Calculate the midpoint angle of the slice\n  const midAngle = (startAngle + endAngle) / 2;\n  // In d3-shape, 0 radians is at 12 o'clock, angles increase clockwise\n  // So the outward direction is: x = sin(angle), y = -cos(angle)\n  return {\n    x: Math.sin(midAngle) * distance,\n    y: -Math.cos(midAngle) * distance,\n  };\n}\n\n/** Hover effect types */\nexport type PieSliceHoverEffect = \"translate\" | \"grow\" | \"none\";\n\nexport interface PieSliceProps {\n  /** Index of the slice in the data array */\n  index: number;\n  /** Optional color override - falls back to data color or palette */\n  color?: string;\n  /** Optional fill override for patterns/gradients (e.g., \"url(#patternId)\") */\n  fill?: string;\n  /** Animate the slice on mount. Default: true */\n  animate?: boolean;\n  /** Show glow effect on hover. Default: true */\n  showGlow?: boolean;\n  /**\n   * Hover effect type. Default: \"translate\"\n   * - \"translate\": Slice moves outward along its radial axis\n   * - \"grow\": Slice extends its outer radius (gets longer)\n   * - \"none\": No hover animation\n   */\n  hoverEffect?: PieSliceHoverEffect;\n  /** Distance in pixels for hover effect (translate distance or grow amount). Defaults to PieChart's hoverOffset */\n  hoverOffset?: number;\n  /** Additional CSS class */\n  className?: string;\n}\n\ninterface AnimatedSliceTranslateProps {\n  index: number;\n  innerRadius: number;\n  outerRadius: number;\n  startAngle: number;\n  endAngle: number;\n  cornerRadius: number;\n  padAngle: number;\n  fill: string;\n  color: string;\n  isHovered: boolean;\n  isFaded: boolean;\n  animationKey: number;\n  showGlow: boolean;\n  hoverOffset: number;\n}\n\nfunction AnimatedSliceTranslate({\n  index,\n  innerRadius,\n  outerRadius,\n  startAngle,\n  endAngle,\n  cornerRadius,\n  padAngle,\n  fill,\n  color,\n  isHovered,\n  isFaded,\n  animationKey,\n  showGlow,\n  hoverOffset,\n}: AnimatedSliceTranslateProps) {\n  const {\n    enterTransition,\n    enterStaggerScale,\n    animationKey: pieAnimationKey,\n  } = usePieStable();\n  const animationDelay = (0.1 + index * 0.08) * enterStaggerScale;\n  const mountProgress = useMountProgress(\n    enterTransition,\n    animationDelay,\n    pieAnimationKey\n  );\n  const enterComplete = useEnterComplete(mountProgress);\n\n  const animatedPath = useTransform(mountProgress, (mount) => {\n    const currentEndAngle = startAngle + (endAngle - startAngle) * mount;\n    if (currentEndAngle <= startAngle + 0.01) {\n      return \"\";\n    }\n    return generateArcPath(\n      innerRadius,\n      outerRadius,\n      startAngle,\n      currentEndAngle,\n      cornerRadius,\n      padAngle\n    );\n  });\n\n  const offset = getSliceOffset(startAngle, endAngle, hoverOffset);\n  const glowColor = color;\n  const hitboxPath = generateArcPath(\n    innerRadius,\n    outerRadius,\n    startAngle,\n    endAngle,\n    cornerRadius,\n    padAngle\n  );\n\n  if (enterComplete) {\n    const shouldTranslate = isHovered;\n    return (\n      <motion.path\n        animate={{\n          opacity: isFaded ? 0.4 : 1,\n          x: shouldTranslate ? offset.x : 0,\n          y: shouldTranslate ? offset.y : 0,\n        }}\n        d={hitboxPath}\n        fill={fill}\n        pointerEvents=\"none\"\n        style={{\n          filter:\n            showGlow && isHovered\n              ? `drop-shadow(0 0 12px ${glowColor})`\n              : \"none\",\n        }}\n        transition={{\n          opacity: { duration: 0.15 },\n          x: { type: \"spring\", stiffness: 400, damping: 25 },\n          y: { type: \"spring\", stiffness: 400, damping: 25 },\n        }}\n      />\n    );\n  }\n\n  return (\n    <motion.path\n      animate={{\n        opacity: isFaded ? 0.4 : 1,\n        x: isHovered ? offset.x : 0,\n        y: isHovered ? offset.y : 0,\n      }}\n      d={animatedPath}\n      fill={fill}\n      key={`slice-${animationKey}-${index}`}\n      pointerEvents=\"none\"\n      style={{\n        filter:\n          showGlow && isHovered ? `drop-shadow(0 0 12px ${glowColor})` : \"none\",\n      }}\n      transition={{\n        opacity: { duration: 0.15 },\n        x: { type: \"spring\", stiffness: 400, damping: 25 },\n        y: { type: \"spring\", stiffness: 400, damping: 25 },\n      }}\n    />\n  );\n}\n\ninterface AnimatedSliceGrowProps {\n  index: number;\n  innerRadius: number;\n  outerRadius: number;\n  startAngle: number;\n  endAngle: number;\n  cornerRadius: number;\n  padAngle: number;\n  fill: string;\n  color: string;\n  isHovered: boolean;\n  isFaded: boolean;\n  animationKey: number;\n  showGlow: boolean;\n  hoverOffset: number;\n}\n\nfunction AnimatedSliceGrow({\n  index,\n  innerRadius,\n  outerRadius,\n  startAngle,\n  endAngle,\n  cornerRadius,\n  padAngle,\n  fill,\n  color,\n  isHovered,\n  isFaded,\n  animationKey,\n  showGlow,\n  hoverOffset,\n}: AnimatedSliceGrowProps) {\n  const {\n    enterTransition,\n    enterStaggerScale,\n    animationKey: pieAnimationKey,\n  } = usePieStable();\n  const animationDelay = (0.1 + index * 0.08) * enterStaggerScale;\n  const mountProgress = useMountProgress(\n    enterTransition,\n    animationDelay,\n    pieAnimationKey\n  );\n  const enterComplete = useEnterComplete(mountProgress);\n\n  const growSpring = useSpring(outerRadius, {\n    stiffness: 400,\n    damping: 25,\n  });\n\n  useEffect(() => {\n    growSpring.set(isHovered ? outerRadius + hoverOffset : outerRadius);\n  }, [isHovered, hoverOffset, outerRadius, growSpring]);\n\n  const animatedPath = useTransform(\n    [mountProgress, growSpring],\n    ([mount, currentOuterRadius]) => {\n      const currentEndAngle =\n        startAngle + (endAngle - startAngle) * (mount as number);\n      if (currentEndAngle <= startAngle + 0.01) {\n        return \"\";\n      }\n      return generateArcPath(\n        innerRadius,\n        currentOuterRadius as number,\n        startAngle,\n        currentEndAngle,\n        cornerRadius,\n        padAngle\n      );\n    }\n  );\n\n  const glowColor = color;\n  const grownOuterRadius = isHovered ? outerRadius + hoverOffset : outerRadius;\n  const grownPath = generateArcPath(\n    innerRadius,\n    grownOuterRadius,\n    startAngle,\n    endAngle,\n    cornerRadius,\n    padAngle\n  );\n\n  if (enterComplete) {\n    return (\n      <motion.path\n        animate={{\n          opacity: isFaded ? 0.4 : 1,\n          d: grownPath,\n        }}\n        d={grownPath}\n        fill={fill}\n        pointerEvents=\"none\"\n        style={{\n          filter:\n            showGlow && isHovered\n              ? `drop-shadow(0 0 12px ${glowColor})`\n              : \"none\",\n        }}\n        transition={{\n          opacity: { duration: 0.15 },\n          d: { type: \"spring\", stiffness: 400, damping: 25 },\n        }}\n      />\n    );\n  }\n\n  return (\n    <motion.path\n      animate={{\n        opacity: isFaded ? 0.4 : 1,\n      }}\n      d={animatedPath}\n      fill={fill}\n      key={`slice-${animationKey}-${index}`}\n      pointerEvents=\"none\"\n      style={{\n        filter:\n          showGlow && isHovered ? `drop-shadow(0 0 12px ${glowColor})` : \"none\",\n      }}\n      transition={{\n        opacity: { duration: 0.15 },\n      }}\n    />\n  );\n}\n\nexport const PieSlice = memo(function PieSlice({\n  index,\n  color: colorProp,\n  fill: fillProp,\n  animate = true,\n  showGlow = true,\n  hoverEffect = \"translate\",\n  hoverOffset: hoverOffsetProp,\n}: PieSliceProps) {\n  const {\n    arcs,\n    innerRadius,\n    outerRadius,\n    cornerRadius,\n    hoverOffset: contextHoverOffset,\n    animationKey,\n    geometryScrubbing,\n    scrubSlicePaths,\n    getColor,\n    getFill,\n  } = usePieStable();\n  const { hoveredIndex, setHoveredIndex } = usePieHover();\n\n  // Use prop if provided, otherwise use context value\n  const hoverOffset = hoverOffsetProp ?? contextHoverOffset;\n\n  const arcData = arcs[index];\n  if (!arcData) {\n    return null;\n  }\n\n  const color = colorProp || getColor(index);\n  const fill = fillProp || getFill(index);\n\n  if (geometryScrubbing) {\n    const scrubPath = scrubSlicePaths?.[index];\n    if (!scrubPath) {\n      return null;\n    }\n    return <path d={scrubPath} fill={fill} pointerEvents=\"none\" />;\n  }\n\n  const isHovered = hoveredIndex === index;\n  const isFaded = hoveredIndex !== null && hoveredIndex !== index;\n\n  // Calculate values for non-animated/static paths\n  const offset = getSliceOffset(\n    arcData.startAngle,\n    arcData.endAngle,\n    hoverOffset\n  );\n\n  // Generate the static hitbox path (always uses base outer radius)\n  const hitboxPath = generateArcPath(\n    innerRadius,\n    outerRadius,\n    arcData.startAngle,\n    arcData.endAngle,\n    cornerRadius,\n    arcData.padAngle\n  );\n\n  // Generate the visible path for grow effect\n  const grownOuterRadius = isHovered ? outerRadius + hoverOffset : outerRadius;\n  const grownPath = generateArcPath(\n    innerRadius,\n    grownOuterRadius,\n    arcData.startAngle,\n    arcData.endAngle,\n    cornerRadius,\n    arcData.padAngle\n  );\n\n  // Render animated slice based on effect type\n  const renderAnimatedSlice = () => {\n    if (hoverEffect === \"grow\") {\n      return (\n        <AnimatedSliceGrow\n          animationKey={animationKey}\n          color={color}\n          cornerRadius={cornerRadius}\n          endAngle={arcData.endAngle}\n          fill={fill}\n          hoverOffset={hoverOffset}\n          index={index}\n          innerRadius={innerRadius}\n          isFaded={isFaded}\n          isHovered={isHovered}\n          outerRadius={outerRadius}\n          padAngle={arcData.padAngle}\n          showGlow={showGlow}\n          startAngle={arcData.startAngle}\n        />\n      );\n    }\n\n    // Default: translate effect (also covers \"none\" with hoverOffset=0)\n    return (\n      <AnimatedSliceTranslate\n        animationKey={animationKey}\n        color={color}\n        cornerRadius={cornerRadius}\n        endAngle={arcData.endAngle}\n        fill={fill}\n        hoverOffset={hoverEffect === \"none\" ? 0 : hoverOffset}\n        index={index}\n        innerRadius={innerRadius}\n        isFaded={isFaded}\n        isHovered={isHovered}\n        outerRadius={outerRadius}\n        padAngle={arcData.padAngle}\n        showGlow={showGlow}\n        startAngle={arcData.startAngle}\n      />\n    );\n  };\n\n  // Render static (non-animated) slice\n  const renderStaticSlice = () => {\n    if (hoverEffect === \"grow\") {\n      return (\n        <motion.path\n          animate={{\n            opacity: isFaded ? 0.4 : 1,\n            d: grownPath,\n          }}\n          d={hitboxPath}\n          fill={fill}\n          pointerEvents=\"none\"\n          style={{\n            filter:\n              showGlow && isHovered ? `drop-shadow(0 0 12px ${color})` : \"none\",\n          }}\n          transition={{\n            opacity: { duration: 0.15 },\n            d: { type: \"spring\", stiffness: 400, damping: 25 },\n          }}\n        />\n      );\n    }\n\n    // Default: translate effect\n    const shouldTranslate = hoverEffect !== \"none\" && isHovered;\n    const translateX = shouldTranslate ? offset.x : 0;\n    const translateY = shouldTranslate ? offset.y : 0;\n\n    return (\n      <motion.path\n        animate={{\n          opacity: isFaded ? 0.4 : 1,\n          x: translateX,\n          y: translateY,\n        }}\n        d={hitboxPath}\n        fill={fill}\n        pointerEvents=\"none\"\n        style={{\n          filter:\n            showGlow && isHovered ? `drop-shadow(0 0 12px ${color})` : \"none\",\n        }}\n        transition={{\n          opacity: { duration: 0.15 },\n          x: { type: \"spring\", stiffness: 400, damping: 25 },\n          y: { type: \"spring\", stiffness: 400, damping: 25 },\n        }}\n      />\n    );\n  };\n\n  return (\n    <g style={{ cursor: \"pointer\" }}>\n      {/* Invisible hitbox - stays in place, handles hover events */}\n      {/* biome-ignore lint/a11y/noStaticElementInteractions: SVG path used as hover hitbox for visualization */}\n      <path\n        d={hitboxPath}\n        fill=\"transparent\"\n        onMouseEnter={() => setHoveredIndex(index)}\n        onMouseLeave={() => setHoveredIndex(null)}\n      />\n\n      {/* Visible slice - animates based on hover effect, no pointer events */}\n      {animate ? renderAnimatedSlice() : renderStaticSlice()}\n    </g>\n  );\n});\n\nPieSlice.displayName = \"PieSlice\";\n\nexport default PieSlice;\n",
      "type": "registry:component",
      "target": "components/charts/pie-slice.tsx"
    },
    {
      "path": "src/charts/chart-stat-flow.tsx",
      "content": "\"use client\";\n\nimport NumberFlow from \"@number-flow/react\";\nimport { type ReactNode, useEffect, useMemo, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\n/** Subset of `Intl.NumberFormatOptions` supported by NumberFlow */\nexport interface ChartStatFlowFormat {\n  notation?: \"standard\" | \"compact\";\n  compactDisplay?: \"short\" | \"long\";\n  minimumFractionDigits?: number;\n  maximumFractionDigits?: number;\n  minimumIntegerDigits?: number;\n  minimumSignificantDigits?: number;\n  maximumSignificantDigits?: number;\n  style?: \"decimal\" | \"percent\" | \"currency\";\n  currency?: string;\n  currencyDisplay?: \"symbol\" | \"narrowSymbol\" | \"code\" | \"name\";\n  unit?: string;\n  unitDisplay?: \"short\" | \"long\" | \"narrow\";\n}\n\nexport const defaultChartStatFlowFormat: ChartStatFlowFormat = {\n  notation: \"standard\",\n  maximumFractionDigits: 0,\n};\n\nfunction formatStatValue(\n  value: number,\n  formatOptions: ChartStatFlowFormat,\n  prefix?: string,\n  suffix?: string\n): string {\n  const formatted = new Intl.NumberFormat(undefined, formatOptions).format(\n    value\n  );\n  return `${prefix ?? \"\"}${formatted}${suffix ?? \"\"}`;\n}\n\nfunction useNumberFlowElementReady(): boolean {\n  const [ready, setReady] = useState(\n    () =>\n      typeof customElements !== \"undefined\" &&\n      Boolean(customElements.get(\"number-flow-react\"))\n  );\n\n  useEffect(() => {\n    if (ready) {\n      return;\n    }\n    let cancelled = false;\n    customElements.whenDefined(\"number-flow-react\").then(() => {\n      if (!cancelled) {\n        setReady(true);\n      }\n    });\n    return () => {\n      cancelled = true;\n    };\n  }, [ready]);\n\n  return ready;\n}\n\nexport interface ChartStatFlowProps {\n  value: number;\n  label: string;\n  formatOptions?: ChartStatFlowFormat;\n  prefix?: string;\n  suffix?: string;\n  valueClassName?: string;\n  labelClassName?: string;\n  icon?: ReactNode;\n}\n\n/**\n * Shared value + label stack using NumberFlow (same layout as pie / ring centers).\n * Parent should provide flex alignment and sizing when needed.\n */\nexport function ChartStatFlow({\n  value,\n  label,\n  formatOptions = defaultChartStatFlowFormat,\n  prefix,\n  suffix,\n  valueClassName = \"text-2xl font-bold\",\n  labelClassName = \"text-xs\",\n  icon,\n}: ChartStatFlowProps) {\n  const numberFlowReady = useNumberFlowElementReady();\n  const staticValue = useMemo(\n    () => formatStatValue(value, formatOptions, prefix, suffix),\n    [value, formatOptions, prefix, suffix]\n  );\n\n  return (\n    <>\n      {icon ? (\n        <div className=\"mb-2 flex h-12 w-12 items-center justify-center rounded-full bg-muted/50\">\n          {icon}\n        </div>\n      ) : null}\n      <span className={cn(\"text-foreground tabular-nums\", valueClassName)}>\n        {numberFlowReady ? (\n          <NumberFlow\n            format={formatOptions}\n            isolate\n            prefix={prefix}\n            suffix={suffix}\n            value={value}\n            willChange\n          />\n        ) : (\n          staticValue\n        )}\n      </span>\n      <span className={cn(\"mt-0.5 text-chart-label\", labelClassName)}>\n        {label}\n      </span>\n    </>\n  );\n}\n\nChartStatFlow.displayName = \"ChartStatFlow\";\n",
      "type": "registry:component",
      "target": "components/charts/chart-stat-flow.tsx"
    },
    {
      "path": "src/charts/pie-center-shell.tsx",
      "content": "\"use client\";\n\nimport { pie as d3Pie } from \"d3-shape\";\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { PieCenter, type PieCenterProps } from \"./pie-center\";\nimport {\n  defaultPieColors,\n  type PieArcData,\n  type PieContextValue,\n  type PieData,\n  PieProvider,\n} from \"./pie-context\";\n\nconst SHELL_HOVER_OFFSET = 10;\n\nexport type PieCenterShellProps = Omit<PieCenterProps, \"children\"> & {\n  /** Value shown with NumberFlow (same role as pie total when not hovering) */\n  centerValue: number;\n  /** Square reference size for pie context (matches `PieChart` `size`) */\n  contextSize: number;\n  /** Inner radius in px — must be > 0 so `PieCenter` renders */\n  innerRadiusPx: number;\n  /**\n   * When true (default), the first paint uses `0` then updates to `centerValue`\n   * on the next frame so NumberFlow can run an entrance transition. Subsequent\n   * `centerValue` updates animate as usual.\n   */\n  animateEntrance?: boolean;\n};\n\n/**\n * Renders {@link PieCenter} with a minimal {@link PieProvider} so you can reuse\n * the same center layout as a donut pie without mounting slices or a full {@link PieChart}.\n */\nexport function PieCenterShell({\n  centerValue,\n  contextSize,\n  innerRadiusPx,\n  animateEntrance = true,\n  ...pieCenterProps\n}: PieCenterShellProps) {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const introStartedRef = useRef(false);\n\n  const [flowTotal, setFlowTotal] = useState(() =>\n    animateEntrance ? 0 : centerValue\n  );\n\n  useEffect(() => {\n    if (!animateEntrance) {\n      setFlowTotal(centerValue);\n      return;\n    }\n\n    if (!introStartedRef.current) {\n      introStartedRef.current = true;\n      setFlowTotal(0);\n      let innerRaf = 0;\n      const outerRaf = requestAnimationFrame(() => {\n        innerRaf = requestAnimationFrame(() => setFlowTotal(centerValue));\n      });\n      return () => {\n        cancelAnimationFrame(outerRaf);\n        cancelAnimationFrame(innerRaf);\n        introStartedRef.current = false;\n      };\n    }\n\n    setFlowTotal(centerValue);\n  }, [animateEntrance, centerValue]);\n\n  const data: PieData[] = useMemo(\n    () => [{ label: \"_pieCenterShell\", value: Math.max(flowTotal, 0) }],\n    [flowTotal]\n  );\n\n  const totalValue = flowTotal;\n\n  const arcs = useMemo((): PieArcData[] => {\n    const v = data[0]?.value ?? 0;\n    if (v > 0) {\n      const pieGenerator = d3Pie<PieData>()\n        .value((d) => d.value)\n        .startAngle(-Math.PI / 2)\n        .endAngle((3 * Math.PI) / 2)\n        .padAngle(0)\n        .sort(null);\n      const computed = pieGenerator(data);\n      return computed.map((arc, index) => ({\n        data: arc.data,\n        index,\n        startAngle: arc.startAngle,\n        endAngle: arc.endAngle,\n        padAngle: arc.padAngle,\n        value: arc.value,\n      })) as PieArcData[];\n    }\n    const d0 = data[0];\n    if (!d0) {\n      return [];\n    }\n    return [\n      {\n        data: d0,\n        index: 0,\n        startAngle: -Math.PI / 2,\n        endAngle: (3 * Math.PI) / 2,\n        padAngle: 0,\n        value: 0,\n      },\n    ];\n  }, [data]);\n\n  const getColor = useCallback((index: number) => {\n    return defaultPieColors[index % defaultPieColors.length] as string;\n  }, []);\n\n  const getFill = useCallback(\n    (index: number) => {\n      const item = data[index];\n      if (item?.fill) {\n        return item.fill;\n      }\n      return getColor(index);\n    },\n    [data, getColor]\n  );\n\n  const center = contextSize / 2;\n  const outerRadius = center - SHELL_HOVER_OFFSET;\n\n  const contextValue: PieContextValue = useMemo(\n    () => ({\n      data,\n      arcs,\n      size: contextSize,\n      center,\n      outerRadius,\n      innerRadius: innerRadiusPx,\n      padAngle: 0,\n      cornerRadius: 0,\n      hoverOffset: SHELL_HOVER_OFFSET,\n      hoveredIndex: null,\n      setHoveredIndex: () => undefined,\n      animationKey: 0,\n      isLoaded: true,\n      enterStaggerScale: 1,\n      containerRef,\n      totalValue,\n      getColor,\n      getFill,\n      geometryScrubbing: false,\n      scrubSlicePaths: null,\n    }),\n    [\n      data,\n      arcs,\n      contextSize,\n      center,\n      outerRadius,\n      innerRadiusPx,\n      totalValue,\n      getColor,\n      getFill,\n    ]\n  );\n\n  return (\n    <PieProvider value={contextValue}>\n      <PieCenter {...pieCenterProps} />\n    </PieProvider>\n  );\n}\n\nPieCenterShell.displayName = \"PieCenterShell\";\n",
      "type": "registry:component",
      "target": "components/charts/pie-center-shell.tsx"
    },
    {
      "path": "src/charts/pie-center.tsx",
      "content": "\"use client\";\n\nimport type { ReactNode } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  chartCenterContainerClassName,\n  chartCenterLabelClassName,\n  chartCenterValueClassName,\n} from \"./chart-center-typography\";\nimport {\n  ChartStatFlow,\n  type ChartStatFlowFormat,\n  defaultChartStatFlowFormat,\n} from \"./chart-stat-flow\";\nimport { usePieHover, usePieStable } from \"./pie-context\";\n\nexport interface PieCenterProps {\n  /** Label shown below the value. Default: \"Total\" when not hovering */\n  defaultLabel?: string;\n  /** Format options for NumberFlow. Default: standard notation */\n  formatOptions?: ChartStatFlowFormat;\n  /** Custom render function for complete control over center content */\n  children?: (props: {\n    value: number;\n    label: string;\n    isHovered: boolean;\n    data: { label: string; value: number; color?: string; fill?: string };\n  }) => ReactNode;\n  /** Additional class name for the container */\n  className?: string;\n  /** Class name for the value text. Scales with center size via container queries. */\n  valueClassName?: string;\n  /** Class name for the label text. Scales with center size via container queries. */\n  labelClassName?: string;\n  /** Prefix to show before the number (e.g., \"$\") */\n  prefix?: string;\n  /** Suffix to show after the number (e.g., \"%\") */\n  suffix?: string;\n}\n\n/**\n * PieCenter displays content in the center of a donut/pie chart.\n *\n * This component renders as pure HTML (not inside SVG foreignObject) to avoid\n * Safari's WebKit bug #23113 where HTML content with CSS transforms/opacity\n * inside foreignObject renders at incorrect positions.\n *\n * The parent PieChart uses CSS Grid stacking to overlay this HTML content\n * on top of the SVG slices.\n */\nexport function PieCenter({\n  defaultLabel = \"Total\",\n  formatOptions = defaultChartStatFlowFormat,\n  children,\n  className = \"\",\n  valueClassName = chartCenterValueClassName,\n  labelClassName = chartCenterLabelClassName,\n  prefix,\n  suffix,\n}: PieCenterProps) {\n  const { data, totalValue, innerRadius, geometryScrubbing } = usePieStable();\n  const { hoveredIndex } = usePieHover();\n\n  const effectiveHoveredIndex = geometryScrubbing ? null : hoveredIndex;\n  const hoveredData =\n    effectiveHoveredIndex === null ? null : data[effectiveHoveredIndex];\n  const displayValue = hoveredData ? hoveredData.value : totalValue;\n  const displayLabel = hoveredData ? hoveredData.label : defaultLabel;\n\n  // Calculate center area size based on inner radius\n  // Leave some padding so text doesn't touch the inner edge\n  const centerSize = innerRadius * 2 - 16;\n\n  // Don't render if there's no inner radius (solid pie, not donut)\n  if (innerRadius <= 0) {\n    return null;\n  }\n\n  // If custom render function is provided, use it\n  if (children && hoveredData) {\n    return (\n      <div\n        className={cn(\n          chartCenterContainerClassName,\n          \"flex items-center justify-center\",\n          className\n        )}\n        style={{ width: centerSize, height: centerSize }}\n      >\n        {children({\n          value: displayValue,\n          label: displayLabel,\n          isHovered: effectiveHoveredIndex !== null,\n          data: hoveredData,\n        })}\n      </div>\n    );\n  }\n\n  // Default center content with NumberFlow animations\n  // Now renders as pure HTML, avoiding Safari's foreignObject bugs\n  return (\n    <div\n      className={cn(\n        chartCenterContainerClassName,\n        \"flex flex-col items-center justify-center text-center\",\n        className\n      )}\n      style={{ width: centerSize, height: centerSize }}\n    >\n      <ChartStatFlow\n        formatOptions={formatOptions}\n        label={displayLabel}\n        labelClassName={labelClassName}\n        prefix={prefix}\n        suffix={suffix}\n        value={displayValue}\n        valueClassName={valueClassName}\n      />\n    </div>\n  );\n}\n\nPieCenter.displayName = \"PieCenter\";\n\nexport default PieCenter;\n",
      "type": "registry:component",
      "target": "components/charts/pie-center.tsx"
    },
    {
      "path": "src/charts/chart-center-typography.ts",
      "content": "/**\n * Fluid typography for pie / ring / gauge center labels.\n *\n * Uses CSS container query units (`cqw`) so values scale with the center\n * hole — not the viewport — which keeps stat text readable on small charts.\n */\nexport const chartCenterContainerClassName =\n  \"@container/chart-center size-full min-w-0\";\n\n/** Primary stat — ~22% of center width, clamped between text-sm and text-3xl. */\nexport const chartCenterValueClassName =\n  \"font-bold tabular-nums leading-none text-[clamp(0.75rem,22cqw,1.875rem)]\";\n\n/** Supporting label — ~9% of center width, clamped between 10px and text-xs. */\nexport const chartCenterLabelClassName =\n  \"max-w-full truncate leading-tight text-[clamp(0.625rem,9cqw,0.75rem)]\";\n",
      "type": "registry:component",
      "target": "components/charts/chart-center-typography.ts"
    }
  ]
}