{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "radar-chart",
  "type": "registry:component",
  "title": "Radar Chart",
  "description": "A composable radar/spider chart with multiple data series",
  "dependencies": [
    "@visx/group@4.0.1-alpha.0",
    "@visx/responsive@4.0.1-alpha.0",
    "@visx/scale@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/radar-chart.tsx",
      "content": "\"use client\";\n\nimport { Group } from \"@visx/group\";\nimport { ParentSize } from \"@visx/responsive\";\nimport { scaleLinear } from \"@visx/scale\";\nimport type { Transition } from \"motion/react\";\nimport { type ReactNode, useCallback, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  defaultRadarColors,\n  type RadarContextValue,\n  type RadarData,\n  type RadarMetric,\n  RadarProvider,\n} from \"./radar-context\";\n\nexport interface RadarChartProps {\n  /** Data array - each item represents a data series (polygon) */\n  data: RadarData[];\n  /** Metrics to display on the radar */\n  metrics: RadarMetric[];\n  /** Chart size in pixels. If not provided, uses parent container size */\n  size?: number;\n  /** Number of concentric grid circles. Default: 5 */\n  levels?: number;\n  /** Margin around the chart. Default: 60 */\n  margin?: number;\n  /** Enable animations. Default: true */\n  animate?: boolean;\n  /** Enter animation budget in ms. Default: 1100 */\n  enterDurationMs?: number;\n  /** Scales stagger timing (1 = default). */\n  staggerScale?: number;\n  /** Motion enter transition (spring or cubic-bezier tween). */\n  enterTransition?: Transition;\n  /** Changes when motion settings change — replays enter animations. */\n  motionReplayKey?: string;\n  /** Controlled hover state - index of hovered area */\n  hoveredIndex?: number | null;\n  /** Callback when hover state changes */\n  onHoverChange?: (index: number | null) => void;\n  /** Additional class name for the container */\n  className?: string;\n  /** Child components (RadarGrid, RadarAxis, RadarLabels, RadarArea) */\n  children: ReactNode;\n}\n\ninterface RadarChartInnerProps {\n  width: number;\n  height: number;\n  data: RadarData[];\n  metrics: RadarMetric[];\n  levels: number;\n  margin: number;\n  animate: boolean;\n  enterDurationMs: number;\n  staggerScale: number;\n  enterTransition?: Transition;\n  motionReplayKey: string;\n  children: ReactNode;\n  hoveredIndexProp?: number | null;\n  onHoverChange?: (index: number | null) => void;\n}\n\nfunction RadarChartInner({\n  width,\n  height,\n  data,\n  metrics,\n  levels,\n  margin,\n  animate,\n  enterDurationMs,\n  staggerScale,\n  enterTransition,\n  motionReplayKey,\n  children,\n  hoveredIndexProp,\n  onHoverChange,\n}: RadarChartInnerProps) {\n  const [internalHoveredIndex, setInternalHoveredIndex] = useState<\n    number | null\n  >(null);\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\n  const size = Math.min(width, height);\n  const radius = (size - margin * 2) / 2;\n\n  // Scale for converting values (0-100) to radius\n  const yScale = useCallback(\n    (value: number) => {\n      const scale = scaleLinear<number>({\n        range: [0, radius],\n        domain: [0, 100],\n      });\n      return scale(value) ?? 0;\n    },\n    [radius]\n  );\n\n  // Get angle for a metric index (rotated so first metric is at top)\n  const getAngle = useCallback(\n    (metricIndex: number) => {\n      const step = (Math.PI * 2) / metrics.length;\n      const angleOffset = -Math.PI / 2; // Rotate so first axis is at top\n      return metricIndex * step + angleOffset;\n    },\n    [metrics.length]\n  );\n\n  // Get x,y position for a metric at a given value\n  const getPointPosition = useCallback(\n    (metricIndex: number, value: number) => {\n      const angle = getAngle(metricIndex);\n      const r = yScale(value);\n      return {\n        x: r * Math.cos(angle),\n        y: r * Math.sin(angle),\n      };\n    },\n    [getAngle, yScale]\n  );\n\n  // Get color for a data index\n  const getColor = useCallback(\n    (index: number) => {\n      const item = data[index];\n      if (item?.color) {\n        return item.color;\n      }\n      return defaultRadarColors[index % defaultRadarColors.length] as string;\n    },\n    [data]\n  );\n\n  // Early return if dimensions not ready\n  if (size < 10) {\n    return null;\n  }\n\n  const contextValue: RadarContextValue = {\n    data,\n    metrics,\n    size,\n    radius,\n    levels,\n    hoveredIndex,\n    setHoveredIndex,\n    animate,\n    enterDurationMs,\n    staggerScale,\n    enterTransition,\n    motionReplayKey,\n    getColor,\n    getAngle,\n    getPointPosition,\n    yScale,\n  };\n\n  return (\n    <RadarProvider value={contextValue}>\n      <svg\n        aria-hidden=\"true\"\n        height={size}\n        style={{ overflow: \"visible\" }}\n        width={size}\n      >\n        <Group left={size / 2} top={size / 2}>\n          {children}\n        </Group>\n      </svg>\n    </RadarProvider>\n  );\n}\n\nexport function RadarChart({\n  data,\n  metrics,\n  size: fixedSize,\n  levels = 5,\n  margin = 60,\n  animate = true,\n  enterDurationMs = 1100,\n  staggerScale = 1,\n  enterTransition,\n  motionReplayKey = \"\",\n  className = \"\",\n  hoveredIndex,\n  onHoverChange,\n  children,\n}: RadarChartProps) {\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        style={{ width: fixedSize, height: fixedSize }}\n      >\n        <RadarChartInner\n          animate={animate}\n          data={data}\n          enterDurationMs={enterDurationMs}\n          enterTransition={enterTransition}\n          height={fixedSize}\n          hoveredIndexProp={hoveredIndex}\n          levels={levels}\n          margin={margin}\n          metrics={metrics}\n          motionReplayKey={motionReplayKey}\n          onHoverChange={onHoverChange}\n          staggerScale={staggerScale}\n          width={fixedSize}\n        >\n          {children}\n        </RadarChartInner>\n      </div>\n    );\n  }\n\n  // Otherwise use ParentSize for responsive sizing\n  return (\n    <div className={cn(\"relative aspect-square w-full\", className)}>\n      <ParentSize debounceTime={10}>\n        {({ width, height }) => (\n          <RadarChartInner\n            animate={animate}\n            data={data}\n            enterDurationMs={enterDurationMs}\n            enterTransition={enterTransition}\n            height={height}\n            hoveredIndexProp={hoveredIndex}\n            levels={levels}\n            margin={margin}\n            metrics={metrics}\n            motionReplayKey={motionReplayKey}\n            onHoverChange={onHoverChange}\n            staggerScale={staggerScale}\n            width={width}\n          >\n            {children}\n          </RadarChartInner>\n        )}\n      </ParentSize>\n    </div>\n  );\n}\n\nexport default RadarChart;\n",
      "type": "registry:component",
      "target": "components/charts/radar-chart.tsx"
    },
    {
      "path": "src/charts/radar-context.tsx",
      "content": "\"use client\";\n\nimport type { Transition } from \"motion/react\";\nimport { createContext, type ReactNode, useContext, useMemo } from \"react\";\n\n// CSS variable references for radar chart theming\nexport const radarCssVars = {\n  background: \"var(--chart-background)\",\n  foreground: \"var(--chart-foreground)\",\n  foregroundMuted: \"var(--chart-foreground-muted)\",\n  label: \"var(--chart-label, oklch(0.65 0.01 260))\",\n  grid: \"var(--chart-grid)\",\n  border: \"var(--border)\",\n  // Default radar colors from chart palette\n  area1: \"var(--chart-1)\",\n  area2: \"var(--chart-2)\",\n  area3: \"var(--chart-3)\",\n  area4: \"var(--chart-4)\",\n  area5: \"var(--chart-5)\",\n};\n\n// Default radar color palette\nexport const defaultRadarColors = [\n  radarCssVars.area1,\n  radarCssVars.area2,\n  radarCssVars.area3,\n  radarCssVars.area4,\n  radarCssVars.area5,\n];\n\nexport interface RadarMetric {\n  /** Unique key for the metric */\n  key: string;\n  /** Display label for the metric */\n  label: string;\n}\n\nexport interface RadarData {\n  /** Display label for this data series */\n  label: string;\n  /** Color for this data series (defaults to chart-1 through chart-5) */\n  color?: string;\n  /** Metric values (key -> value, normalized 0-100) */\n  values: Record<string, number>;\n}\n\nexport interface RadarHoverContextValue {\n  hoveredIndex: number | null;\n  setHoveredIndex: (index: number | null) => void;\n}\n\nexport interface RadarStableContextValue {\n  // Data\n  data: RadarData[];\n  metrics: RadarMetric[];\n\n  // Dimensions\n  size: number;\n  radius: number;\n  levels: number;\n\n  // Animation\n  animate: boolean;\n  /** Total enter animation budget in ms */\n  enterDurationMs: number;\n  /** Scales stagger delays between grid / campaigns / metrics */\n  staggerScale: number;\n  /** Motion enter transition (spring or cubic-bezier tween). */\n  enterTransition?: Transition;\n  /** Changes when motion settings change — replays enter animations. */\n  motionReplayKey: string;\n\n  // Computed helpers\n  getColor: (index: number) => string;\n  getAngle: (metricIndex: number) => number;\n  getPointPosition: (\n    metricIndex: number,\n    value: number\n  ) => { x: number; y: number };\n  yScale: (value: number) => number;\n}\n\nexport type RadarContextValue = RadarStableContextValue &\n  RadarHoverContextValue;\n\nconst RadarStableContext = createContext<RadarStableContextValue | null>(null);\nconst RadarHoverContext = createContext<RadarHoverContextValue | null>(null);\n\nexport function RadarProvider({\n  children,\n  value,\n}: {\n  children: ReactNode;\n  value: RadarContextValue;\n}) {\n  const stable = useMemo<RadarStableContextValue>(\n    () => ({\n      data: value.data,\n      metrics: value.metrics,\n      size: value.size,\n      radius: value.radius,\n      levels: value.levels,\n      animate: value.animate,\n      enterDurationMs: value.enterDurationMs,\n      staggerScale: value.staggerScale,\n      enterTransition: value.enterTransition,\n      motionReplayKey: value.motionReplayKey,\n      getColor: value.getColor,\n      getAngle: value.getAngle,\n      getPointPosition: value.getPointPosition,\n      yScale: value.yScale,\n    }),\n    [\n      value.data,\n      value.metrics,\n      value.size,\n      value.radius,\n      value.levels,\n      value.animate,\n      value.enterDurationMs,\n      value.staggerScale,\n      value.enterTransition,\n      value.motionReplayKey,\n      value.getColor,\n      value.getAngle,\n      value.getPointPosition,\n      value.yScale,\n    ]\n  );\n\n  const hover = useMemo<RadarHoverContextValue>(\n    () => ({\n      hoveredIndex: value.hoveredIndex,\n      setHoveredIndex: value.setHoveredIndex,\n    }),\n    [value.hoveredIndex, value.setHoveredIndex]\n  );\n\n  return (\n    <RadarStableContext.Provider value={stable}>\n      <RadarHoverContext.Provider value={hover}>\n        {children}\n      </RadarHoverContext.Provider>\n    </RadarStableContext.Provider>\n  );\n}\n\nexport function useRadarStable(): RadarStableContextValue {\n  const context = useContext(RadarStableContext);\n  if (!context) {\n    throw new Error(\n      \"useRadarStable must be used within a RadarProvider. \" +\n        \"Make sure your component is wrapped in <RadarChart>.\"\n    );\n  }\n  return context;\n}\n\nexport function useRadarHover(): RadarHoverContextValue {\n  const context = useContext(RadarHoverContext);\n  if (!context) {\n    throw new Error(\n      \"useRadarHover must be used within a RadarProvider. \" +\n        \"Make sure your component is wrapped in <RadarChart>.\"\n    );\n  }\n  return context;\n}\n\nexport function useRadar(): RadarContextValue {\n  return { ...useRadarStable(), ...useRadarHover() };\n}\n\nexport default RadarStableContext;\n",
      "type": "registry:component",
      "target": "components/charts/radar-context.tsx"
    },
    {
      "path": "src/charts/radar-area.tsx",
      "content": "\"use client\";\n\nimport type { MotionValue } from \"motion/react\";\nimport { motion, useTransform } from \"motion/react\";\nimport { memo, useMemo } from \"react\";\nimport { radarCssVars, useRadarHover, useRadarStable } from \"./radar-context\";\nimport { useEnterComplete } from \"./use-enter-complete\";\nimport { useMountProgress } from \"./use-mount-progress\";\n\nexport interface RadarAreaProps {\n  /** Index of this area in the data array */\n  index: number;\n  /** Optional color override */\n  color?: string;\n  /** Show data point circles. Default: true */\n  showPoints?: boolean;\n  /** Show stroke outline on the polygon. Default: true */\n  showStroke?: boolean;\n  /** Show glow effect on hover. Default: true */\n  showGlow?: boolean;\n  /** Additional class name */\n  className?: string;\n}\n\nfunction getStrokeWidth(isHovered: boolean): number {\n  return isHovered ? 3 : 2;\n}\n\nfunction positionsToPath(positions: { x: number; y: number }[]): string {\n  if (positions.length === 0) {\n    return \"\";\n  }\n  return `M ${positions.map((p) => `${p.x},${p.y}`).join(\" L \")} Z`;\n}\n\nconst RadarPoint = memo(function RadarPoint({\n  mountProgress,\n  target,\n  color,\n  isHovered,\n  metricKey,\n  enterComplete,\n}: {\n  mountProgress: MotionValue<number>;\n  target: { x: number; y: number };\n  color: string;\n  isHovered: boolean;\n  metricKey: string;\n  enterComplete: boolean;\n}) {\n  const cx = useTransform(mountProgress, (t) => target.x * t);\n  const cy = useTransform(mountProgress, (t) => target.y * t);\n\n  if (enterComplete) {\n    return (\n      <circle\n        cx={target.x}\n        cy={target.y}\n        fill={color}\n        key={metricKey}\n        r={isHovered ? 6 : 4}\n        stroke={radarCssVars.background}\n        strokeWidth={2}\n      />\n    );\n  }\n\n  return (\n    <motion.circle\n      cx={cx}\n      cy={cy}\n      fill={color}\n      key={metricKey}\n      r={isHovered ? 6 : 4}\n      stroke={radarCssVars.background}\n      strokeWidth={2}\n      transition={{\n        r: { type: \"spring\", stiffness: 300, damping: 20 },\n      }}\n    />\n  );\n});\n\n// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: radar area enter/hover branches mirror pie slice\nexport const RadarArea = memo(function RadarArea({\n  index,\n  color: colorProp,\n  showPoints = true,\n  showStroke = true,\n  showGlow = true,\n  className = \"\",\n}: RadarAreaProps) {\n  const {\n    data,\n    metrics,\n    levels,\n    animate,\n    enterDurationMs,\n    staggerScale,\n    enterTransition,\n    motionReplayKey,\n    getColor,\n    getPointPosition,\n  } = useRadarStable();\n  const { hoveredIndex, setHoveredIndex } = useRadarHover();\n\n  const durationFactor = enterDurationMs / 1100;\n  const areaData = data[index];\n\n  const targetPositions = useMemo(() => {\n    if (!areaData) {\n      return metrics.map(() => ({ x: 0, y: 0 }));\n    }\n    return metrics.map((metric, i) => {\n      const value = areaData.values[metric.key] ?? 0;\n      return getPointPosition(i, value);\n    });\n  }, [metrics, areaData, getPointPosition]);\n\n  const staticPath = useMemo(\n    () => positionsToPath(targetPositions),\n    [targetPositions]\n  );\n\n  const gridStagger = 0.08 * staggerScale * durationFactor;\n  const campaignBaseDelay = (levels * gridStagger + 0.2) * durationFactor;\n  const campaignStagger = 0.15 * staggerScale * durationFactor;\n  const animationDelay = campaignBaseDelay + index * campaignStagger;\n\n  const mountProgress = useMountProgress(\n    enterTransition,\n    animationDelay,\n    `${motionReplayKey}-${index}`\n  );\n  const enterComplete = useEnterComplete(mountProgress);\n\n  const animatedPositions = useTransform(mountProgress, (t) =>\n    targetPositions.map((p) => ({ x: p.x * t, y: p.y * t }))\n  );\n\n  const pathD = useTransform(animatedPositions, positionsToPath);\n\n  if (!areaData) {\n    return null;\n  }\n\n  const color = colorProp || getColor(index);\n  const isHovered = hoveredIndex === index;\n  const isOtherHovered = hoveredIndex !== null && hoveredIndex !== index;\n\n  return (\n    <motion.g\n      animate={{\n        opacity: isOtherHovered ? 0.3 : 1,\n        scale: isHovered ? 1.05 : 1,\n      }}\n      className={className}\n      initial={{ opacity: animate ? 0 : 1 }}\n      onMouseEnter={() => setHoveredIndex(index)}\n      onMouseLeave={() => setHoveredIndex(null)}\n      style={{ transformOrigin: \"0px 0px\", cursor: \"pointer\" }}\n      transition={{\n        opacity: { duration: 0.15 },\n        scale: { type: \"spring\", stiffness: 400, damping: 25 },\n      }}\n    >\n      {enterComplete ? (\n        <path\n          d={staticPath}\n          fill={color}\n          fillOpacity={isHovered ? 0.35 : 0.15}\n          stroke={showStroke ? color : \"none\"}\n          strokeLinejoin=\"round\"\n          strokeWidth={showStroke ? getStrokeWidth(isHovered) : 0}\n          style={{\n            filter:\n              showGlow && isHovered ? `drop-shadow(0 0 12px ${color})` : \"none\",\n          }}\n        />\n      ) : (\n        <motion.path\n          animate={{\n            fillOpacity: isHovered ? 0.35 : 0.15,\n            strokeWidth: showStroke ? getStrokeWidth(isHovered) : 0,\n          }}\n          d={pathD}\n          fill={color}\n          stroke={showStroke ? color : \"none\"}\n          strokeLinejoin=\"round\"\n          style={{\n            filter:\n              showGlow && isHovered ? `drop-shadow(0 0 12px ${color})` : \"none\",\n          }}\n          transition={{\n            fillOpacity: { duration: 0.2 },\n            strokeWidth: { duration: 0.2 },\n          }}\n        />\n      )}\n\n      {showPoints &&\n        metrics.map((metric, i) => {\n          const target = targetPositions[i];\n          if (!target) {\n            return null;\n          }\n          return (\n            <RadarPoint\n              color={color}\n              enterComplete={enterComplete}\n              isHovered={isHovered}\n              key={metric.key}\n              metricKey={metric.key}\n              mountProgress={mountProgress}\n              target={target}\n            />\n          );\n        })}\n    </motion.g>\n  );\n});\n\nRadarArea.displayName = \"RadarArea\";\n\nexport default RadarArea;\n",
      "type": "registry:component",
      "target": "components/charts/radar-area.tsx"
    },
    {
      "path": "src/charts/radar-axis.tsx",
      "content": "\"use client\";\n\nimport { motion } from \"motion/react\";\nimport { radarCssVars, useRadarStable } from \"./radar-context\";\n\nexport interface RadarAxisProps {\n  /** Axis line stroke color. Default: var(--border) */\n  stroke?: string;\n  /** Axis line stroke opacity. Default: 0.6 */\n  strokeOpacity?: number;\n  /** Additional class name */\n  className?: string;\n}\n\nexport function RadarAxis({\n  stroke = radarCssVars.border,\n  strokeOpacity = 0.6,\n  className = \"\",\n}: RadarAxisProps) {\n  const { metrics, radius, getAngle, animate } = useRadarStable();\n\n  // Animation delay base\n  const axisBaseDelay = 0;\n\n  return (\n    <g className={className}>\n      {metrics.map((metric, i) => {\n        const angle = getAngle(i);\n        const targetX = radius * Math.cos(angle);\n        const targetY = radius * Math.sin(angle);\n\n        return (\n          <motion.line\n            animate={{ x2: targetX, y2: targetY }}\n            initial={animate ? { x2: 0, y2: 0 } : { x2: targetX, y2: targetY }}\n            key={`axis-${metric.key}`}\n            stroke={stroke}\n            strokeOpacity={strokeOpacity}\n            strokeWidth={1}\n            transition={{\n              type: \"spring\",\n              stiffness: 80,\n              damping: 15,\n              mass: 1,\n              delay: animate ? axisBaseDelay + i * 0.05 : 0,\n            }}\n            x1={0}\n            y1={0}\n          />\n        );\n      })}\n    </g>\n  );\n}\n\nRadarAxis.displayName = \"RadarAxis\";\n\nexport default RadarAxis;\n",
      "type": "registry:component",
      "target": "components/charts/radar-axis.tsx"
    },
    {
      "path": "src/charts/radar-grid.tsx",
      "content": "\"use client\";\n\nimport { scaleLinear } from \"@visx/scale\";\nimport { LineRadial } from \"@visx/shape\";\nimport { motion } from \"motion/react\";\nimport { transitionWithDelay } from \"./motion-utils\";\nimport { radarCssVars, useRadarStable } from \"./radar-context\";\n\nexport interface RadarGridProps {\n  /** Show level value labels. Default: true */\n  showLabels?: boolean;\n  /** Grid line stroke color. Default: var(--border) */\n  stroke?: string;\n  /** Grid line stroke opacity. Default: 0.6 */\n  strokeOpacity?: number;\n  /** Additional class name */\n  className?: string;\n}\n\nexport function RadarGrid({\n  showLabels = true,\n  stroke = radarCssVars.border,\n  strokeOpacity = 0.6,\n  className = \"\",\n}: RadarGridProps) {\n  const {\n    metrics,\n    radius,\n    levels,\n    animate,\n    enterTransition,\n    staggerScale,\n    enterDurationMs,\n    motionReplayKey,\n  } = useRadarStable();\n\n  const durationFactor = enterDurationMs / 1100;\n  const gridStagger = 0.08 * staggerScale * durationFactor;\n\n  // Generate angles for the radial lines (one per metric)\n  const degrees = 360;\n  const angles = [...new Array(metrics.length + 1)].map((_, i) => ({\n    angle: i * (degrees / metrics.length) + degrees / metrics.length / 2,\n  }));\n\n  // Radial scale for converting degrees to radians\n  const radialScale = scaleLinear<number>({\n    range: [0, Math.PI * 2],\n    domain: [degrees, 0],\n  });\n\n  const labelDelay = levels * gridStagger * 0.5;\n\n  return (\n    <g className={className}>\n      {/* Concentric grid circles */}\n      {[...new Array(levels)].map((_, i) => {\n        const targetRadius = ((i + 1) * radius) / levels;\n        return (\n          <motion.g\n            animate={{ scale: 1, opacity: 1 }}\n            initial={\n              animate ? { scale: 0, opacity: 0 } : { scale: 1, opacity: 1 }\n            }\n            // biome-ignore lint/suspicious/noArrayIndexKey: Static grid levels\n            key={`grid-${i}-${motionReplayKey}`}\n            style={{ transformOrigin: \"0px 0px\" }}\n            transition={\n              animate\n                ? transitionWithDelay(enterTransition, i * gridStagger, {\n                    type: \"spring\",\n                    stiffness: 100,\n                    damping: 15,\n                    mass: 1,\n                  })\n                : undefined\n            }\n          >\n            <LineRadial\n              angle={(d) => radialScale(d.angle) ?? 0}\n              data={angles}\n              fill=\"none\"\n              radius={targetRadius}\n              stroke={stroke}\n              strokeLinecap=\"round\"\n              strokeOpacity={strokeOpacity}\n              strokeWidth={1}\n            />\n          </motion.g>\n        );\n      })}\n\n      {/* Grid level labels */}\n      {showLabels &&\n        [...new Array(levels)].map((_, i) => (\n          <motion.g\n            animate={{ opacity: 1 }}\n            initial={animate ? { opacity: 0 } : { opacity: 1 }}\n            // biome-ignore lint/suspicious/noArrayIndexKey: Static grid levels\n            key={`level-label-${i}-${motionReplayKey}`}\n            transition={\n              animate\n                ? transitionWithDelay(\n                    enterTransition,\n                    labelDelay + i * 0.06 * durationFactor\n                  )\n                : undefined\n            }\n          >\n            <text\n              dominantBaseline=\"middle\"\n              fill={radarCssVars.foregroundMuted}\n              fontSize={9}\n              textAnchor=\"start\"\n              x={4}\n              y={-((i + 1) * radius) / levels}\n            >\n              {((i + 1) * 100) / levels}\n            </text>\n          </motion.g>\n        ))}\n    </g>\n  );\n}\n\nRadarGrid.displayName = \"RadarGrid\";\n\nexport default RadarGrid;\n",
      "type": "registry:component",
      "target": "components/charts/radar-grid.tsx"
    },
    {
      "path": "src/charts/radar-labels.tsx",
      "content": "\"use client\";\n\nimport { motion } from \"motion/react\";\nimport { radarCssVars, useRadarStable } from \"./radar-context\";\n\nexport interface RadarLabelsProps {\n  /** Distance from the chart edge. Default: 24 */\n  offset?: number;\n  /** Font size for labels. Default: 11 */\n  fontSize?: number;\n  /** Enable interactive hover on labels. Default: false */\n  interactive?: boolean;\n  /** Additional class name */\n  className?: string;\n}\n\nexport function RadarLabels({\n  offset = 24,\n  fontSize = 11,\n  interactive = false,\n  className = \"\",\n}: RadarLabelsProps) {\n  const { metrics, radius, levels, getAngle, animate } = useRadarStable();\n\n  // Label animation delay (starts after grid begins)\n  const gridStagger = 0.08;\n  const labelDelay = levels * gridStagger * 0.5;\n\n  const labelRadius = radius + offset;\n\n  return (\n    <g className={className}>\n      {metrics.map((metric, i) => {\n        const angle = getAngle(i);\n        const x = labelRadius * Math.cos(angle);\n        const y = labelRadius * Math.sin(angle);\n\n        return (\n          <motion.g\n            animate={{ opacity: 1, x, y }}\n            initial={\n              animate ? { opacity: 0, x: 0, y: 0 } : { opacity: 1, x, y }\n            }\n            key={`label-${metric.key}`}\n            transition={{\n              opacity: {\n                duration: 0.5,\n                delay: animate ? labelDelay + i * 0.08 : 0,\n              },\n              x: { type: \"spring\", stiffness: 80, damping: 15 },\n              y: { type: \"spring\", stiffness: 80, damping: 15 },\n            }}\n          >\n            <text\n              className={\n                interactive\n                  ? \"cursor-pointer transition-opacity duration-150 hover:opacity-100\"\n                  : \"\"\n              }\n              dominantBaseline=\"middle\"\n              fontSize={fontSize}\n              fontWeight={500}\n              opacity={interactive ? 0.8 : 1}\n              style={{ fill: radarCssVars.label }}\n              textAnchor=\"middle\"\n              x={0}\n              y={0}\n            >\n              {metric.label}\n            </text>\n          </motion.g>\n        );\n      })}\n    </g>\n  );\n}\n\nRadarLabels.displayName = \"RadarLabels\";\n\nexport default RadarLabels;\n",
      "type": "registry:component",
      "target": "components/charts/radar-labels.tsx"
    }
  ]
}