{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "recharts-radar-chart",
  "description": "Radar chart component with filled and lines variants",
  "dependencies": [
    "recharts",
    "motion"
  ],
  "registryDependencies": [
    "@evilcharts/recharts-chart",
    "@evilcharts/recharts-tooltip",
    "@evilcharts/recharts-legend",
    "@evilcharts/recharts-dot",
    "@evilcharts/recharts-background"
  ],
  "files": [
    {
      "path": "src/registry/charts/recharts-radar-chart.tsx",
      "content": "\"use client\";\n\nimport {\n  PolarAngleAxis as RechartsPolarAngleAxis,\n  PolarGrid as RechartsPolarGrid,\n  PolarRadiusAxis as RechartsPolarRadiusAxis,\n  Radar as RechartsRadar,\n  RadarChart as RechartsRadarChart,\n} from \"recharts\";\nimport {\n  Children,\n  createContext,\n  isValidElement,\n  use,\n  useCallback,\n  useEffect,\n  useId,\n  useMemo,\n  useState,\n  type ComponentProps,\n  type FC,\n  type ReactElement,\n  type ReactNode,\n} from \"react\";\nimport {\n  ChartTooltip,\n  ChartTooltipContent,\n  type TooltipRoundness,\n  type TooltipVariant,\n} from \"@/registry/ui/recharts-tooltip\";\nimport {\n  type ChartConfig,\n  ChartContainer,\n  getColorsCount,\n  LoadingIndicator,\n} from \"@/registry/ui/recharts-chart\";\nimport { ChartLegend, ChartLegendContent, type ChartLegendVariant } from \"@/registry/ui/recharts-legend\";\nimport { ChartBackground, type BackgroundVariant } from \"@/registry/ui/recharts-background\";\nimport { ChartDot, type DotVariant } from \"@/registry/ui/recharts-dot\";\n\n// Constants\nconst STROKE_WIDTH = 1;\nconst DEFAULT_FILL_OPACITY = 0.3;\nconst LOADING_POINTS = 6;\nconst LOADING_ANIMATION_DURATION = 1500; // in milliseconds\nconst LOADING_RADAR_DATA_KEY = \"value\";\n\ntype RadarVariant = \"filled\" | \"lines\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Shared context\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Shared state for every part of the chart. Lifted into <EvilRadarChart /> so that\n * <Radar />, <PolarAngleAxis />, <Legend />, and friends can read it without prop\n * drilling. Sub-components are composed freely — the provider is the single source\n * of truth.\n */\ntype RadarChartContextValue = {\n  config: ChartConfig; // colors + labels for every series\n  isLoading: boolean; // whether the chart shows its loading skeleton\n  selectedDataKey: string | null; // currently selected series, or null when none\n  selectDataKey: (dataKey: string | null) => void; // sets the selected series\n};\n\nconst RadarChartContext = createContext<RadarChartContextValue | null>(null);\n\n// Reads the chart context, throwing a helpful error when used outside <EvilRadarChart />\nfunction useRadarChart() {\n  const context = use(RadarChartContext);\n\n  if (!context) {\n    throw new Error(\n      \"Radar chart parts (<Radar />, <PolarAngleAxis />, …) must be used within <EvilRadarChart />\",\n    );\n  }\n\n  return context;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Root container\n// ─────────────────────────────────────────────────────────────────────────────\n\n// Validates that every config key also exists on the data row type\ntype ValidateConfigKeys<TData, TConfig> = {\n  [K in keyof TConfig]: K extends keyof TData ? ChartConfig[string] : never;\n};\n\ntype EvilRadarChartBaseProps<\n  TData extends Record<string, unknown>,\n  TConfig extends Record<string, ChartConfig[string]>,\n> = {\n  config: TConfig & ValidateConfigKeys<TData, TConfig>; // series colors + labels\n  data: TData[]; // rows rendered by the chart\n  children: ReactNode; // composed parts — <Radar />, <PolarGrid />, <Legend />, …\n  className?: string; // extra classes for the chart container\n  chartProps?: ComponentProps<typeof RechartsRadarChart>; // escape hatch for the raw Recharts chart\n  backgroundVariant?: BackgroundVariant; // background pattern drawn behind the chart\n  defaultSelectedDataKey?: string | null; // series selected on first render\n  onSelectionChange?: (selectedDataKey: string | null) => void; // fires when the selected series changes\n  isLoading?: boolean; // shows the animated loading skeleton\n  loadingPoints?: number; // number of points in the loading skeleton\n};\n\ntype EvilRadarChartProps<\n  TData extends Record<string, unknown>,\n  TConfig extends Record<string, ChartConfig[string]>,\n> = EvilRadarChartBaseProps<TData, TConfig>;\n\n/**\n * Root of the composible radar chart. Owns the data, the shared context, and the\n * loading skeleton. Everything visual — the polar grid, axes, tooltip, legend, and\n * the radars themselves — is composed as children, so a consumer renders exactly\n * the parts they need.\n */\nexport function EvilRadarChart<\n  TData extends Record<string, unknown>,\n  TConfig extends Record<string, ChartConfig[string]>,\n>({\n  config,\n  data,\n  children,\n  className,\n  chartProps,\n  backgroundVariant,\n  defaultSelectedDataKey = null,\n  onSelectionChange,\n  isLoading = false,\n  loadingPoints,\n}: EvilRadarChartProps<TData, TConfig>) {\n  const chartId = useId().replace(/:/g, \"\"); // colon-free id keeps CSS/SVG selectors valid\n  const [selectedDataKey, setSelectedDataKey] = useState<string | null>(defaultSelectedDataKey);\n  const loadingData = useLoadingData(isLoading, loadingPoints);\n\n  // Updates selection state and notifies the parent\n  const selectDataKey = useCallback(\n    (newSelectedDataKey: string | null) => {\n      setSelectedDataKey(newSelectedDataKey);\n      onSelectionChange?.(newSelectedDataKey);\n    },\n    [onSelectionChange],\n  );\n\n  const contextValue = useMemo<RadarChartContextValue>(\n    () => ({\n      config,\n      isLoading,\n      selectedDataKey,\n      selectDataKey,\n    }),\n    [config, isLoading, selectedDataKey, selectDataKey],\n  );\n\n  return (\n    <RadarChartContext value={contextValue}>\n      <ChartContainer className={className} config={config}>\n        <LoadingIndicator isLoading={isLoading} />\n        <RechartsRadarChart id={chartId} data={isLoading ? loadingData : data} {...chartProps}>\n          {backgroundVariant && <ChartBackground variant={backgroundVariant} />}\n          {children}\n          {isLoading && <LoadingRadar />}\n        </RechartsRadarChart>\n      </ChartContainer>\n    </RadarChartContext>\n  );\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Composible parts\n// ─────────────────────────────────────────────────────────────────────────────\n\ntype RadarProps = {\n  dataKey: string; // series key — must exist on the data and config\n  variant?: RadarVariant; // fill style for this radar only\n  fillOpacity?: number; // opacity of the filled area when `variant=\"filled\"`\n  isGlowing?: boolean; // adds a soft outer glow around this radar\n  isClickable?: boolean; // lets this radar be selected by clicking it\n  children?: ReactNode; // optional <Dot /> and <ActiveDot /> composition\n  radarProps?: Omit<ComponentProps<typeof RechartsRadar>, \"dataKey\">; // escape hatch for raw Recharts Radar props\n};\n\n/**\n * A single radar series. Each <Radar /> is fully self-contained: it generates its\n * own stroke/fill gradients and glow filter under a unique id, so any number of\n * radars — each with its own variant, opacity, and clickability — can live in one\n * chart without style collisions. Compose <Dot /> and <ActiveDot /> inside it to\n * add point markers.\n */\nfunction Radar({\n  dataKey,\n  variant = \"filled\",\n  fillOpacity = DEFAULT_FILL_OPACITY,\n  isGlowing = false,\n  isClickable = false,\n  children,\n  radarProps,\n}: RadarProps) {\n  const { config, isLoading, selectedDataKey, selectDataKey } = useRadarChart();\n  const id = useId().replace(/:/g, \"\"); // unique id scopes this radar's style defs\n\n  // The root renders the skeleton radar while loading, so real radars step aside\n  if (isLoading) return null;\n\n  const isSelected = selectedDataKey === null || selectedDataKey === dataKey;\n  const isDimmed = isClickable && !isSelected;\n  // Opacity when another radar is selected. The stroke stays full (1) on the\n  // selected/normal radar; when dimmed the fill recedes twice as far as the\n  // stroke and dots (fill 0.1 vs 0.2) so the picked radar reads clearly. The\n  // fill value multiplies the `fillOpacity` prop; stroke/dot are absolute.\n  const opacity = {\n    stroke: isDimmed ? 0.2 : 1,\n    fill: isDimmed ? 0.1 : 1,\n    dot: isDimmed ? 0.2 : 1,\n  };\n  const isFilled = variant === \"filled\";\n\n  const { dot, activeDot } = resolveDots(children, id, dataKey, opacity.dot);\n\n  return (\n    <>\n      <RechartsRadar\n        dataKey={dataKey}\n        stroke={`url(#${id}-radar-stroke-${dataKey})`}\n        strokeOpacity={opacity.stroke}\n        strokeWidth={STROKE_WIDTH}\n        fill={isFilled ? `url(#${id}-radar-fill-${dataKey})` : \"none\"}\n        fillOpacity={isFilled ? fillOpacity * opacity.fill : 0}\n        dot={dot}\n        activeDot={activeDot}\n        filter={isGlowing ? `url(#${id}-radar-glow-${dataKey})` : undefined}\n        className=\"transition-opacity duration-200\"\n        style={isClickable ? { cursor: \"pointer\" } : undefined}\n        onClick={() => {\n          if (!isClickable) return;\n          // Clicking the selected radar clears the selection, otherwise selects it\n          selectDataKey(selectedDataKey === dataKey ? null : dataKey);\n        }}\n        {...radarProps}\n      />\n      <defs>\n        <ColorGradient id={id} dataKey={dataKey} config={config} />\n        <StrokeGradient id={id} dataKey={dataKey} config={config} />\n        {isFilled && <FillGradient id={id} dataKey={dataKey} config={config} />}\n        {isGlowing && <GlowFilter id={id} dataKey={dataKey} />}\n      </defs>\n    </>\n  );\n}\n\ntype DotProps = {\n  variant?: DotVariant; // visual style of the point marker\n};\n\n/**\n * Declares a resting point marker for the <Radar /> it is composed inside.\n * It renders nothing on its own — the parent <Radar /> reads its variant and\n * wires it into the Recharts dot slot.\n */\nconst Dot: FC<DotProps> = () => null;\n\n/**\n * Declares the hovered/active point marker for the <Radar /> it is composed\n * inside. Like <Dot />, it is a configuration slot and renders nothing itself.\n */\nconst ActiveDot: FC<DotProps> = () => null;\n\ntype PolarGridProps = ComponentProps<typeof RechartsPolarGrid>;\n\n/**\n * The polar grid lines. Defaults to a dashed polygon grid and forwards every\n * Recharts PolarGrid prop, so `gridType`, `polarRadius`, etc. pass straight through.\n */\nfunction PolarGrid({\n  gridType = \"polygon\",\n  stroke = \"currentColor\",\n  strokeOpacity = 0.2,\n  strokeDasharray = \"3 4\",\n  ...props\n}: PolarGridProps) {\n  return (\n    <RechartsPolarGrid\n      gridType={gridType}\n      stroke={stroke}\n      strokeOpacity={strokeOpacity}\n      strokeDasharray={strokeDasharray}\n      {...props}\n    />\n  );\n}\n\ntype PolarAngleAxisProps = ComponentProps<typeof RechartsPolarAngleAxis>;\n\n/**\n * The angular category axis — the labels around the chart's perimeter. Ships\n * with the chart's flat default styling and forwards every Recharts\n * PolarAngleAxis prop. Hidden automatically while the chart is loading.\n */\nfunction PolarAngleAxis({\n  tick = { fill: \"currentColor\", fontSize: 12 },\n  tickLine = false,\n  ...props\n}: PolarAngleAxisProps) {\n  const { isLoading } = useRadarChart();\n\n  if (isLoading) return null;\n\n  return <RechartsPolarAngleAxis tick={tick} tickLine={tickLine} {...props} />;\n}\n\ntype PolarRadiusAxisProps = ComponentProps<typeof RechartsPolarRadiusAxis>;\n\n/**\n * The radial value axis — the scale running from the center outward. Forwards\n * every Recharts PolarRadiusAxis prop. Hidden automatically while the chart is\n * loading.\n */\nfunction PolarRadiusAxis({\n  tick = { fill: \"currentColor\", fontSize: 10 },\n  tickLine = false,\n  axisLine = false,\n  ...props\n}: PolarRadiusAxisProps) {\n  const { isLoading } = useRadarChart();\n\n  if (isLoading) return null;\n\n  return <RechartsPolarRadiusAxis tick={tick} tickLine={tickLine} axisLine={axisLine} {...props} />;\n}\n\ntype TooltipProps = {\n  variant?: TooltipVariant; // visual style of the tooltip surface\n  roundness?: TooltipRoundness; // border-radius of the tooltip\n  defaultIndex?: number; // data index shown by default with no hover\n};\n\n/**\n * The hover tooltip. Reads the chart's selection from context so its content\n * dims unselected series. Hidden automatically while the chart is loading.\n */\nfunction Tooltip({ variant, roundness, defaultIndex }: TooltipProps) {\n  const { isLoading, selectedDataKey } = useRadarChart();\n\n  if (isLoading) return null;\n\n  return (\n    <ChartTooltip\n      defaultIndex={defaultIndex}\n      cursor={false}\n      content={\n        <ChartTooltipContent selected={selectedDataKey} roundness={roundness} variant={variant} />\n      }\n    />\n  );\n}\n\ntype LegendProps = {\n  variant?: ChartLegendVariant; // visual style of the legend indicators\n  align?: \"left\" | \"center\" | \"right\"; // horizontal placement\n  verticalAlign?: \"top\" | \"middle\" | \"bottom\"; // vertical placement\n  isClickable?: boolean; // lets each entry toggle selection of its series\n};\n\n/**\n * The series legend. When `isClickable` is set, each entry toggles selection of\n * its series, driving the shared selection state read by every <Radar />.\n * Hidden automatically while the chart is loading.\n */\nfunction Legend({\n  variant,\n  align = \"center\",\n  verticalAlign = \"bottom\",\n  isClickable = false,\n}: LegendProps) {\n  const { isLoading, selectedDataKey, selectDataKey } = useRadarChart();\n\n  if (isLoading) return null;\n\n  return (\n    <ChartLegend\n      verticalAlign={verticalAlign}\n      align={align}\n      content={\n        <ChartLegendContent\n          selected={selectedDataKey}\n          onSelectChange={selectDataKey}\n          isClickable={isClickable}\n          variant={variant}\n        />\n      }\n    />\n  );\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Dot helpers\n// ─────────────────────────────────────────────────────────────────────────────\n\ntype RadarDotProp = ComponentProps<typeof RechartsRadar>[\"dot\"];\ntype RadarActiveDotProp = ComponentProps<typeof RechartsRadar>[\"activeDot\"];\n\n// Pulls <Dot /> and <ActiveDot /> out of a radar's children into Recharts dot slots\nconst resolveDots = (\n  children: ReactNode,\n  id: string,\n  dataKey: string,\n  dotOpacity: number,\n): { dot: RadarDotProp; activeDot: RadarActiveDotProp } => {\n  let dot: RadarDotProp = false;\n  let activeDot: RadarActiveDotProp = false;\n\n  Children.forEach(children, (child) => {\n    if (!isValidElement(child)) return;\n\n    if (child.type === Dot) {\n      const { variant } = (child as ReactElement<DotProps>).props;\n      dot = <ChartDot type={variant} dataKey={dataKey} chartId={id} fillOpacity={dotOpacity} />;\n    }\n\n    if (child.type === ActiveDot) {\n      const { variant } = (child as ReactElement<DotProps>).props;\n      activeDot = (\n        <ChartDot type={variant} dataKey={dataKey} chartId={id} fillOpacity={dotOpacity} />\n      );\n    }\n  });\n\n  return { dot, activeDot };\n};\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Style definitions — one set per <Radar />, scoped to its unique id\n// ─────────────────────────────────────────────────────────────────────────────\n\ntype StyleProps = {\n  id: string; // unique id of the owning <Radar />\n  dataKey: string; // series key the styles belong to\n  config: ChartConfig; // colors + labels for every series\n};\n\ntype ColorStopsProps = {\n  dataKey: string; // series key the stops belong to\n  colorsCount: number; // number of color steps in the gradient\n  opacities?: number[]; // optional per-stop opacity ramp\n};\n\n// Emits one <stop> per color, falling back to a flat gradient for single colors\nconst ColorStops = ({ dataKey, colorsCount, opacities }: ColorStopsProps) => {\n  if (colorsCount === 1) {\n    return (\n      <>\n        <stop offset=\"0%\" stopColor={`var(--color-${dataKey}-0)`} stopOpacity={opacities?.[0]} />\n        <stop\n          offset=\"100%\"\n          stopColor={`var(--color-${dataKey}-0)`}\n          stopOpacity={opacities?.[opacities.length - 1]}\n        />\n      </>\n    );\n  }\n\n  return (\n    <>\n      {Array.from({ length: colorsCount }, (_, index) => {\n        const offset = `${(index / (colorsCount - 1)) * 100}%`;\n        return (\n          <stop\n            key={offset}\n            offset={offset}\n            stopColor={`var(--color-${dataKey}-${index}, var(--color-${dataKey}-0))`}\n            stopOpacity={opacities?.[index]}\n          />\n        );\n      })}\n    </>\n  );\n};\n\n/**\n * Horizontal left-to-right color gradient for a series. Always rendered — the\n * radar's dots paint from this single gradient.\n */\nconst ColorGradient = ({ id, dataKey, config }: StyleProps) => {\n  const colorsCount = getColorsCount(config[dataKey] ?? {});\n\n  return (\n    <linearGradient id={`${id}-colors-${dataKey}`} x1=\"0\" y1=\"0\" x2=\"1\" y2=\"0\">\n      <ColorStops dataKey={dataKey} colorsCount={colorsCount} />\n    </linearGradient>\n  );\n};\n\n/** Diagonal color gradient used for the radar's outline stroke. */\nconst StrokeGradient = ({ id, dataKey, config }: StyleProps) => {\n  const colorsCount = getColorsCount(config[dataKey] ?? {});\n\n  return (\n    <linearGradient id={`${id}-radar-stroke-${dataKey}`} x1=\"0\" y1=\"0\" x2=\"1\" y2=\"1\">\n      <ColorStops dataKey={dataKey} colorsCount={colorsCount} />\n    </linearGradient>\n  );\n};\n\n/** Radial color gradient used for the radar's filled area, fading toward the edge. */\nconst FillGradient = ({ id, dataKey, config }: StyleProps) => {\n  const colorsCount = getColorsCount(config[dataKey] ?? {});\n  const opacities =\n    colorsCount === 1\n      ? [0.8, 0.3]\n      : Array.from({ length: colorsCount }, (_, i) => (i === 0 ? 0.8 : 0.3));\n\n  return (\n    <radialGradient id={`${id}-radar-fill-${dataKey}`} cx=\"50%\" cy=\"50%\" r=\"50%\">\n      <ColorStops dataKey={dataKey} colorsCount={colorsCount} opacities={opacities} />\n    </radialGradient>\n  );\n};\n\n/** Soft outer glow filter applied to a radar when `isGlowing` is set. */\nconst GlowFilter = ({ id, dataKey }: Pick<StyleProps, \"id\" | \"dataKey\">) => {\n  return (\n    <filter id={`${id}-radar-glow-${dataKey}`} x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\">\n      <feGaussianBlur in=\"SourceGraphic\" stdDeviation=\"4\" result=\"blur\" />\n      <feColorMatrix\n        in=\"blur\"\n        type=\"matrix\"\n        values=\"1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 0.6 0\"\n        result=\"glow\"\n      />\n      <feMerge>\n        <feMergeNode in=\"glow\" />\n        <feMergeNode in=\"SourceGraphic\" />\n      </feMerge>\n    </filter>\n  );\n};\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Loading skeleton\n// ─────────────────────────────────────────────────────────────────────────────\n\n// Builds a fresh set of randomized loading points for the skeleton radar\nconst generateLoadingData = (points: number) => {\n  const categories = [\"A\", \"B\", \"C\", \"D\", \"E\", \"F\"];\n\n  return categories.slice(0, points).map((category) => ({\n    skill: category,\n    [LOADING_RADAR_DATA_KEY]: 30 + Math.random() * 70,\n  }));\n};\n\n/**\n * Hook that regenerates the loading skeleton data on a fixed interval, so the\n * skeleton radar keeps animating between shapes while the chart is loading.\n */\nexport function useLoadingData(isLoading: boolean, loadingPoints: number = LOADING_POINTS) {\n  const [refreshKey, setRefreshKey] = useState(0);\n\n  useEffect(() => {\n    if (!isLoading) return;\n\n    const interval = setInterval(() => {\n      setRefreshKey((prev) => prev + 1);\n    }, LOADING_ANIMATION_DURATION);\n\n    return () => clearInterval(interval);\n  }, [isLoading]);\n\n  const loadingData = useMemo(\n    () => generateLoadingData(loadingPoints),\n    // refreshKey toggle triggers re-computation each animation cycle\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n    [loadingPoints, refreshKey],\n  );\n\n  return loadingData;\n}\n\n/**\n * The skeleton radar shown while the chart is loading. Rendered by the root in\n * place of the real radars, it animates between randomized shapes.\n */\nconst LoadingRadar = () => {\n  return (\n    <RechartsRadar\n      dataKey={LOADING_RADAR_DATA_KEY}\n      stroke=\"currentColor\"\n      strokeOpacity={0.3}\n      strokeWidth={2}\n      fill=\"currentColor\"\n      fillOpacity={0.1}\n      dot={false}\n      isAnimationActive\n      animationDuration={LOADING_ANIMATION_DURATION}\n      animationEasing=\"ease-in-out\"\n    />\n  );\n};\n\n// Compound API: every part hangs off the root as a static member, so a consumer\n// writes <EvilRadarChart.Radar/>, <EvilRadarChart.Tooltip/>, … from a single import\n// — no colliding named marker exports when several charts share one file.\nEvilRadarChart.Radar = Radar;\nEvilRadarChart.Dot = Dot;\nEvilRadarChart.ActiveDot = ActiveDot;\nEvilRadarChart.PolarGrid = PolarGrid;\nEvilRadarChart.PolarAngleAxis = PolarAngleAxis;\nEvilRadarChart.PolarRadiusAxis = PolarRadiusAxis;\nEvilRadarChart.Tooltip = Tooltip;\nEvilRadarChart.Legend = Legend;\n",
      "type": "registry:component",
      "target": "components/evilcharts/charts/recharts-radar-chart.tsx"
    }
  ],
  "type": "registry:component"
}