{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ring-chart",
  "type": "registry:component",
  "title": "Ring Chart",
  "description": "A composable donut/ring chart with progress indicators",
  "dependencies": [
    "@visx/group@4.0.1-alpha.0",
    "@visx/responsive@4.0.1-alpha.0",
    "@visx/shape@4.0.1-alpha.0",
    "@number-flow/react",
    "motion"
  ],
  "registryDependencies": [
    "@bklit/chart-animation",
    "@bklit/utils"
  ],
  "files": [
    {
      "path": "src/charts/ring-chart.tsx",
      "content": "\"use client\";\n\nimport { Group } from \"@visx/group\";\nimport { ParentSize } from \"@visx/responsive\";\nimport { arc as arcGenerator } from \"@visx/shape\";\nimport type { Transition } from \"motion/react\";\nimport {\n  Children,\n  isValidElement,\n  memo,\n  type ReactNode,\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  defaultRingColors,\n  type RingContextValue,\n  type RingData,\n  RingProvider,\n  ringCssVars,\n} from \"./ring-context\";\n\nfunction generateRingArcPath(\n  innerRadius: number,\n  outerRadius: number,\n  startAngle: number,\n  endAngle: number,\n  cornerRadius: number\n): string {\n  const generator = arcGenerator<unknown>({\n    innerRadius,\n    outerRadius,\n    cornerRadius,\n  });\n  return generator({ startAngle, endAngle } as unknown as null) || \"\";\n}\n\nexport interface RingChartProps {\n  /** Data array - each item represents a ring */\n  data: RingData[];\n  /** Chart size in pixels. If not provided, uses parent container size */\n  size?: number;\n  /** Stroke width of each ring. Default: 12 */\n  strokeWidth?: number;\n  /** Gap between rings. Default: 6 */\n  ringGap?: number;\n  /** Inner radius of the innermost ring. Default: 60 */\n  baseInnerRadius?: number;\n  /** Animation duration in milliseconds. Default: 1100 */\n  animationDuration?: number;\n  /** Additional class name for the container */\n  className?: string;\n  /** Controlled hover state - index of hovered ring */\n  hoveredIndex?: number | null;\n  /** Callback when hover state changes */\n  onHoverChange?: (index: number | null) => void;\n  /** Start angle in radians. Default: -PI/2 (top) */\n  startAngle?: number;\n  /** End angle in radians. Default: 3*PI/2 (full circle) */\n  endAngle?: number;\n  /** Framer Motion transition for ring enter animation */\n  enterTransition?: Transition;\n  /** Scales ring 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` morphing.\n   */\n  geometryScrubbing?: boolean;\n  /** Child components (Ring, RingCenter, etc.) */\n  children: ReactNode;\n}\n\ninterface RingChartInnerProps {\n  width: number;\n  height: number;\n  data: RingData[];\n  strokeWidth: number;\n  ringGap: number;\n  baseInnerRadius: number;\n  children: ReactNode;\n  containerRef: React.RefObject<HTMLDivElement | null>;\n  hoveredIndexProp?: number | null;\n  onHoverChange?: (index: number | null) => void;\n  startAngle: number;\n  endAngle: number;\n  enterTransition?: Transition;\n  enterStaggerScale: number;\n  geometryScrubbing: boolean;\n}\n\nfunction isRing(child: ReactNode): boolean {\n  return (\n    isValidElement(child) &&\n    typeof child.type === \"function\" &&\n    ((child.type as { displayName?: string }).displayName === \"Ring\" ||\n      (child.type as { name?: string }).name === \"Ring\")\n  );\n}\n\n// Helper to check if a child is a RingCenter component\nfunction isRingCenter(child: ReactNode): boolean {\n  return (\n    isValidElement(child) &&\n    typeof child.type === \"function\" &&\n    ((child.type as { displayName?: string }).displayName === \"RingCenter\" ||\n      child.type.name === \"RingCenter\")\n  );\n}\n\nfunction RingChartInner(props: RingChartInnerProps) {\n  const size = Math.min(props.width, props.height);\n\n  if (size < 10) {\n    return null;\n  }\n\n  return <RingChartCore {...props} />;\n}\n\ninterface ScrubRingLayer {\n  bgPath: string;\n  progressPath: string;\n  color: string;\n}\n\nconst RingChartCore = memo(function RingChartCore({\n  width,\n  height,\n  data,\n  strokeWidth: strokeWidthProp,\n  ringGap: ringGapProp,\n  baseInnerRadius: baseInnerRadiusProp,\n  children,\n  containerRef,\n  hoveredIndexProp,\n  onHoverChange,\n  startAngle,\n  endAngle,\n  enterTransition,\n  enterStaggerScale,\n  geometryScrubbing,\n}: RingChartInnerProps) {\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 scaled dimensions to fit within the available space\n  // The outermost ring needs to fit within the chart with some padding\n  const ringCount = data.length;\n  const padding = 8; // Padding from edge\n  const availableRadius = center - padding;\n\n  // Calculate the \"design\" outer radius (what we'd need at 1:1 scale)\n  const designOuterRadius =\n    baseInnerRadiusProp +\n    (ringCount - 1) * (strokeWidthProp + ringGapProp) +\n    strokeWidthProp;\n\n  // Scale factor to fit within available space\n  const scale = Math.min(1, availableRadius / designOuterRadius);\n\n  // Apply scaling to all dimensions\n  const strokeWidth = strokeWidthProp * scale;\n  const ringGap = ringGapProp * scale;\n  const baseInnerRadius = baseInnerRadiusProp * scale;\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 ring index\n  const getColor = useCallback(\n    (index: number) => {\n      const item = data[index];\n      if (item?.color) {\n        return item.color;\n      }\n      return defaultRingColors[index % defaultRingColors.length] as string;\n    },\n    [data]\n  );\n\n  // Get ring radii for an index\n  const getRingRadii = useCallback(\n    (index: number) => {\n      const innerRadius = baseInnerRadius + index * (strokeWidth + ringGap);\n      const outerRadius = innerRadius + strokeWidth;\n      return { innerRadius, outerRadius };\n    },\n    [baseInnerRadius, strokeWidth, ringGap]\n  );\n\n  const arcRange = endAngle - startAngle;\n  const scrubRingLayers = useMemo((): readonly ScrubRingLayer[] | null => {\n    if (!geometryScrubbing) {\n      return null;\n    }\n    return data.map((ringData, index) => {\n      const { innerRadius, outerRadius } = getRingRadii(index);\n      const cornerRadius = (outerRadius - innerRadius) / 2;\n      const progress = ringData.value / ringData.maxValue;\n      const progressEndAngle = startAngle + arcRange * progress;\n      return {\n        bgPath: generateRingArcPath(\n          innerRadius,\n          outerRadius,\n          startAngle,\n          endAngle,\n          cornerRadius\n        ),\n        progressPath:\n          progressEndAngle <= startAngle + 0.01\n            ? \"\"\n            : generateRingArcPath(\n                innerRadius,\n                outerRadius,\n                startAngle,\n                progressEndAngle,\n                cornerRadius\n              ),\n        color: getColor(index),\n      };\n    });\n  }, [\n    geometryScrubbing,\n    data,\n    getRingRadii,\n    getColor,\n    startAngle,\n    endAngle,\n    arcRange,\n  ]);\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 SVG children (rings) from HTML children (RingCenter)\n  // This avoids Safari's foreignObject positioning bugs (WebKit #23113)\n  const { svgChildren, centerChildren } = useMemo(() => {\n    const svgNodes: ReactNode[] = [];\n    const centerNodes: ReactNode[] = [];\n\n    Children.forEach(children, (child) => {\n      if (isRingCenter(child)) {\n        centerNodes.push(child);\n      } else if (geometryScrubbing && isRing(child)) {\n        return;\n      } else {\n        svgNodes.push(child);\n      }\n    });\n\n    return { svgChildren: svgNodes, centerChildren: centerNodes };\n  }, [children, geometryScrubbing]);\n\n  const contextValue: RingContextValue = useMemo(\n    () => ({\n      data,\n      size,\n      center,\n      strokeWidth,\n      ringGap,\n      baseInnerRadius,\n      hoveredIndex,\n      setHoveredIndex,\n      animationKey,\n      isLoaded: effectiveIsLoaded,\n      enterTransition,\n      enterStaggerScale,\n      containerRef,\n      totalValue,\n      getColor,\n      getRingRadii,\n      startAngle,\n      endAngle,\n      geometryScrubbing,\n    }),\n    [\n      data,\n      size,\n      center,\n      strokeWidth,\n      ringGap,\n      baseInnerRadius,\n      hoveredIndex,\n      setHoveredIndex,\n      animationKey,\n      effectiveIsLoaded,\n      enterTransition,\n      enterStaggerScale,\n      containerRef,\n      totalValue,\n      getColor,\n      getRingRadii,\n      startAngle,\n      endAngle,\n      geometryScrubbing,\n    ]\n  );\n\n  // Use CSS Grid stacking to layer SVG and HTML content\n  // This avoids Safari's foreignObject rendering bugs where HTML content\n  // inside SVG foreignObject renders at wrong positions when it has a RenderLayer\n  return (\n    <RingProvider 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 rings */}\n        <svg\n          aria-hidden=\"true\"\n          height={size}\n          style={{ gridArea: \"1 / 1\", contain: \"layout style paint\" }}\n          width={size}\n        >\n          <Group left={center} top={center}>\n            {scrubRingLayers\n              ? scrubRingLayers.map((layer, index) => (\n                  <g key={data[index]?.label ?? index}>\n                    <path d={layer.bgPath} fill={ringCssVars.ringBackground} />\n                    {layer.progressPath ? (\n                      <path d={layer.progressPath} fill={layer.color} />\n                    ) : null}\n                  </g>\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    </RingProvider>\n  );\n}, ringChartCorePropsEqual);\n\nfunction ringChartCorePropsEqual(\n  prev: RingChartInnerProps,\n  next: RingChartInnerProps\n): boolean {\n  return (\n    prev.width === next.width &&\n    prev.height === next.height &&\n    prev.data === next.data &&\n    prev.strokeWidth === next.strokeWidth &&\n    prev.ringGap === next.ringGap &&\n    prev.baseInnerRadius === next.baseInnerRadius &&\n    prev.hoveredIndexProp === next.hoveredIndexProp &&\n    prev.onHoverChange === next.onHoverChange &&\n    prev.startAngle === next.startAngle &&\n    prev.endAngle === next.endAngle &&\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 RingChart({\n  data,\n  size: fixedSize,\n  strokeWidth = 12,\n  ringGap = 6,\n  baseInnerRadius = 60,\n  className = \"\",\n  hoveredIndex,\n  onHoverChange,\n  startAngle = -Math.PI / 2,\n  endAngle = (3 * Math.PI) / 2,\n  enterTransition,\n  enterStaggerScale = 1,\n  geometryScrubbing = false,\n  children,\n}: RingChartProps) {\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        <RingChartInner\n          baseInnerRadius={baseInnerRadius}\n          containerRef={containerRef}\n          data={data}\n          endAngle={endAngle}\n          enterStaggerScale={enterStaggerScale}\n          enterTransition={enterTransition}\n          geometryScrubbing={geometryScrubbing}\n          height={fixedSize}\n          hoveredIndexProp={hoveredIndex}\n          onHoverChange={onHoverChange}\n          ringGap={ringGap}\n          startAngle={startAngle}\n          strokeWidth={strokeWidth}\n          width={fixedSize}\n        >\n          {children}\n        </RingChartInner>\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          <RingChartInner\n            baseInnerRadius={baseInnerRadius}\n            containerRef={containerRef}\n            data={data}\n            endAngle={endAngle}\n            enterStaggerScale={enterStaggerScale}\n            enterTransition={enterTransition}\n            geometryScrubbing={geometryScrubbing}\n            height={height}\n            hoveredIndexProp={hoveredIndex}\n            onHoverChange={onHoverChange}\n            ringGap={ringGap}\n            startAngle={startAngle}\n            strokeWidth={strokeWidth}\n            width={width}\n          >\n            {children}\n          </RingChartInner>\n        )}\n      </ParentSize>\n    </div>\n  );\n}\n\nexport default RingChart;\n",
      "type": "registry:component",
      "target": "components/charts/ring-chart.tsx"
    },
    {
      "path": "src/charts/ring-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 ring chart theming\nexport const ringCssVars = {\n  background: \"var(--chart-background)\",\n  foreground: \"var(--chart-foreground)\",\n  foregroundMuted: \"var(--chart-foreground-muted)\",\n  label: \"var(--chart-label)\",\n  ringBackground: \"var(--border)\",\n  // Default ring colors from chart palette\n  ring1: \"var(--chart-1)\",\n  ring2: \"var(--chart-2)\",\n  ring3: \"var(--chart-3)\",\n  ring4: \"var(--chart-4)\",\n  ring5: \"var(--chart-5)\",\n};\n\n// Default ring color palette\nexport const defaultRingColors = [\n  ringCssVars.ring1,\n  ringCssVars.ring2,\n  ringCssVars.ring3,\n  ringCssVars.ring4,\n  ringCssVars.ring5,\n];\n\nexport interface RingData {\n  /** Display label for the ring */\n  label: string;\n  /** Current value */\n  value: number;\n  /** Maximum value (determines progress percentage) */\n  maxValue: number;\n  /** Optional color override - falls back to palette */\n  color?: string;\n}\n\nexport interface RingHoverContextValue {\n  hoveredIndex: number | null;\n  setHoveredIndex: (index: number | null) => void;\n}\n\nexport interface RingStableContextValue {\n  // Data\n  data: RingData[];\n\n  // Dimensions\n  size: number;\n  center: number;\n  strokeWidth: number;\n  ringGap: number;\n  baseInnerRadius: 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 ring index\n  getColor: (index: number) => string;\n\n  // Get ring radii for an index\n  getRingRadii: (index: number) => { innerRadius: number; outerRadius: number };\n\n  // Arc angle range\n  startAngle: number;\n  endAngle: number;\n\n  /**\n   * Studio geometry scrub — skip Motion path morphing and use plain SVG paths.\n   * @default false\n   */\n  geometryScrubbing: boolean;\n}\n\nexport type RingContextValue = RingStableContextValue & RingHoverContextValue;\n\nconst RingStableContext = createContext<RingStableContextValue | null>(null);\nconst RingHoverContext = createContext<RingHoverContextValue | null>(null);\n\nexport function RingProvider({\n  children,\n  value,\n}: {\n  children: ReactNode;\n  value: RingContextValue;\n}) {\n  const stable = useMemo<RingStableContextValue>(\n    () => ({\n      data: value.data,\n      size: value.size,\n      center: value.center,\n      strokeWidth: value.strokeWidth,\n      ringGap: value.ringGap,\n      baseInnerRadius: value.baseInnerRadius,\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      getRingRadii: value.getRingRadii,\n      startAngle: value.startAngle,\n      endAngle: value.endAngle,\n      geometryScrubbing: value.geometryScrubbing,\n    }),\n    [\n      value.data,\n      value.size,\n      value.center,\n      value.strokeWidth,\n      value.ringGap,\n      value.baseInnerRadius,\n      value.animationKey,\n      value.isLoaded,\n      value.enterTransition,\n      value.enterStaggerScale,\n      value.containerRef,\n      value.totalValue,\n      value.getColor,\n      value.getRingRadii,\n      value.startAngle,\n      value.endAngle,\n      value.geometryScrubbing,\n    ]\n  );\n\n  const hover = useMemo<RingHoverContextValue>(\n    () => ({\n      hoveredIndex: value.hoveredIndex,\n      setHoveredIndex: value.setHoveredIndex,\n    }),\n    [value.hoveredIndex, value.setHoveredIndex]\n  );\n\n  return (\n    <RingStableContext.Provider value={stable}>\n      <RingHoverContext.Provider value={hover}>\n        {children}\n      </RingHoverContext.Provider>\n    </RingStableContext.Provider>\n  );\n}\n\nexport function useRingStable(): RingStableContextValue {\n  const context = useContext(RingStableContext);\n  if (!context) {\n    throw new Error(\n      \"useRingStable must be used within a RingProvider. \" +\n        \"Make sure your component is wrapped in <RingChart>.\"\n    );\n  }\n  return context;\n}\n\nexport function useRingHover(): RingHoverContextValue {\n  const context = useContext(RingHoverContext);\n  if (!context) {\n    throw new Error(\n      \"useRingHover must be used within a RingProvider. \" +\n        \"Make sure your component is wrapped in <RingChart>.\"\n    );\n  }\n  return context;\n}\n\nexport function useRing(): RingContextValue {\n  return { ...useRingStable(), ...useRingHover() };\n}\n\nexport default RingStableContext;\n",
      "type": "registry:component",
      "target": "components/charts/ring-context.tsx"
    },
    {
      "path": "src/charts/ring.tsx",
      "content": "\"use client\";\n\nimport { arc as arcGenerator } from \"@visx/shape\";\nimport { type MotionValue, motion, useTransform } from \"motion/react\";\nimport { memo, useCallback } from \"react\";\nimport { ringCssVars, useRingHover, useRingStable } from \"./ring-context\";\nimport { useEnterComplete } from \"./use-enter-complete\";\nimport { useMountProgress } from \"./use-mount-progress\";\n\nfunction generateArcPath(\n  innerRadius: number,\n  outerRadius: number,\n  startAngle: number,\n  endAngle: number,\n  cornerRadius: number\n): string {\n  const generator = arcGenerator<unknown>({\n    innerRadius,\n    outerRadius,\n    cornerRadius,\n  });\n  return generator({ startAngle, endAngle } as unknown as null) || \"\";\n}\n\nexport type RingLineCap = \"round\" | \"butt\";\n\nexport interface RingProps {\n  index: number;\n  color?: string;\n  animate?: boolean;\n  showGlow?: boolean;\n  lineCap?: RingLineCap;\n}\n\nfunction ringHoverScale(isHovered: boolean, isPushedOut: boolean): number {\n  if (isHovered) {\n    return 1.03;\n  }\n  if (isPushedOut) {\n    return 1.02;\n  }\n  return 1;\n}\n\nfunction RingProgressPath({\n  progressComplete,\n  progressPath,\n  animatedProgressPath,\n  color,\n}: {\n  progressComplete: boolean;\n  progressPath: string;\n  animatedProgressPath: MotionValue<string>;\n  color: string;\n}) {\n  if (progressComplete) {\n    if (!progressPath) {\n      return null;\n    }\n    return <path d={progressPath} fill={color} />;\n  }\n  return <motion.path d={animatedProgressPath} fill={color} />;\n}\n\nexport const Ring = memo(function Ring({\n  index,\n  color: colorProp,\n  animate = true,\n  showGlow = true,\n  lineCap = \"round\",\n}: RingProps) {\n  const {\n    data,\n    getColor,\n    getRingRadii,\n    startAngle,\n    endAngle,\n    enterTransition,\n    enterStaggerScale,\n    animationKey,\n  } = useRingStable();\n  const { hoveredIndex, setHoveredIndex } = useRingHover();\n\n  const expandDelay = index * 0.08 * enterStaggerScale;\n  const expandProgress = useMountProgress(\n    enterTransition,\n    expandDelay,\n    `${animationKey}-expand-${index}`\n  );\n  const expandComplete = useEnterComplete(expandProgress);\n\n  const progressDelay = (0.6 + index * 0.1) * enterStaggerScale;\n  const progressMount = useMountProgress(\n    enterTransition,\n    progressDelay,\n    `${animationKey}-progress-${index}`\n  );\n  const progressComplete = useEnterComplete(progressMount);\n\n  const ringData = data[index];\n  const progress = ringData ? ringData.value / ringData.maxValue : 0;\n  const arcRange = endAngle - startAngle;\n\n  const animatedProgressPath = useTransform(progressMount, (v) => {\n    if (!ringData) {\n      return \"\";\n    }\n    const currentEndAngle = startAngle + arcRange * progress * v;\n    if (currentEndAngle <= startAngle + 0.01) {\n      return \"\";\n    }\n    const radii = getRingRadii(index);\n    const corner =\n      lineCap === \"round\" ? (radii.outerRadius - radii.innerRadius) / 2 : 0;\n    return generateArcPath(\n      radii.innerRadius,\n      radii.outerRadius,\n      startAngle,\n      currentEndAngle,\n      corner\n    );\n  });\n\n  const enterScale = useTransform(expandProgress, [0, 1], [0, 1]);\n\n  const handleMouseEnter = useCallback(\n    () => setHoveredIndex(index),\n    [index, setHoveredIndex]\n  );\n  const handleMouseLeave = useCallback(\n    () => setHoveredIndex(null),\n    [setHoveredIndex]\n  );\n\n  if (!ringData) {\n    return null;\n  }\n\n  const { innerRadius, outerRadius } = getRingRadii(index);\n  const color = colorProp || getColor(index);\n\n  const isHovered = hoveredIndex === index;\n  const isFaded = hoveredIndex !== null && hoveredIndex !== index;\n  const isPushedOut = hoveredIndex !== null && hoveredIndex < index;\n\n  const cornerRadius =\n    lineCap === \"round\" ? (outerRadius - innerRadius) / 2 : 0;\n  const bgPath = generateArcPath(\n    innerRadius,\n    outerRadius,\n    startAngle,\n    endAngle,\n    cornerRadius\n  );\n  const progressEndAngle = startAngle + arcRange * progress;\n  const progressPath =\n    progressEndAngle <= startAngle + 0.01\n      ? \"\"\n      : generateArcPath(\n          innerRadius,\n          outerRadius,\n          startAngle,\n          progressEndAngle,\n          cornerRadius\n        );\n\n  const hoverScale = ringHoverScale(isHovered, isPushedOut);\n  const layerOpacity = isFaded ? 0.35 : 1;\n  const enterDone = !animate || (expandComplete && progressComplete);\n\n  const groupStyle = {\n    cursor: \"pointer\" as const,\n    transformOrigin: \"0px 0px\",\n    filter: showGlow && isHovered ? `drop-shadow(0 0 12px ${color})` : \"none\",\n  };\n\n  if (enterDone) {\n    return (\n      <motion.g\n        animate={{ scale: hoverScale, opacity: layerOpacity }}\n        onMouseEnter={handleMouseEnter}\n        onMouseLeave={handleMouseLeave}\n        style={groupStyle}\n        transition={{\n          scale: { type: \"spring\", stiffness: 400, damping: 25 },\n          opacity: { duration: 0.15 },\n        }}\n      >\n        <path d={bgPath} fill={ringCssVars.ringBackground} />\n        {progressPath ? <path d={progressPath} fill={color} /> : null}\n      </motion.g>\n    );\n  }\n\n  if (!expandComplete) {\n    return (\n      <motion.g\n        onMouseEnter={handleMouseEnter}\n        onMouseLeave={handleMouseLeave}\n        style={{\n          ...groupStyle,\n          scale: enterScale,\n          opacity: layerOpacity,\n        }}\n      >\n        <path d={bgPath} fill={ringCssVars.ringBackground} />\n      </motion.g>\n    );\n  }\n\n  return (\n    <motion.g\n      animate={{ scale: hoverScale, opacity: layerOpacity }}\n      onMouseEnter={handleMouseEnter}\n      onMouseLeave={handleMouseLeave}\n      style={groupStyle}\n      transition={{\n        scale: { type: \"spring\", stiffness: 400, damping: 25 },\n        opacity: { duration: 0.15 },\n      }}\n    >\n      <path d={bgPath} fill={ringCssVars.ringBackground} />\n      <RingProgressPath\n        animatedProgressPath={animatedProgressPath}\n        color={color}\n        progressComplete={progressComplete}\n        progressPath={progressPath}\n      />\n    </motion.g>\n  );\n});\n\nRing.displayName = \"Ring\";\n\nexport default Ring;\n",
      "type": "registry:component",
      "target": "components/charts/ring.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/ring-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 { useRingHover, useRingStable } from \"./ring-context\";\n\nexport interface RingCenterProps {\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; maxValue: number; color?: 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 * RingCenter displays content in the center of the ring 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 RingChart uses CSS Grid stacking to overlay this HTML content\n * on top of the SVG rings.\n */\nexport function RingCenter({\n  defaultLabel = \"Total\",\n  formatOptions = defaultChartStatFlowFormat,\n  children,\n  className = \"\",\n  valueClassName = chartCenterValueClassName,\n  labelClassName = chartCenterLabelClassName,\n  prefix,\n  suffix,\n}: RingCenterProps) {\n  const { data, totalValue, baseInnerRadius } = useRingStable();\n  const { hoveredIndex } = useRingHover();\n\n  const hoveredData = hoveredIndex === null ? null : data[hoveredIndex];\n  const displayValue = hoveredData ? hoveredData.value : totalValue;\n  const displayLabel = hoveredData ? hoveredData.label : defaultLabel;\n\n  // Calculate center area size based on scaled baseInnerRadius\n  // Leave some padding so text doesn't touch the inner ring\n  const centerSize = baseInnerRadius * 2 - 16;\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: hoveredIndex !== 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\nRingCenter.displayName = \"RingCenter\";\n\nexport default RingCenter;\n",
      "type": "registry:component",
      "target": "components/charts/ring-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"
    }
  ]
}