{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "radar-chart",
  "title": "Radar Chart",
  "description": "A filled, outlined, or dotted radar chart with polygon or circle grids, for scores and multi-series comparisons.",
  "dependencies": [
    "recharts"
  ],
  "registryDependencies": [
    "chart"
  ],
  "files": [
    {
      "path": "registry/dashboardcn/ui/radar-chart.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n  PolarAngleAxis,\n  PolarGrid,\n  PolarRadiusAxis,\n  Radar,\n  RadarChart as RechartsRadarChart,\n} from \"recharts\"\n\nimport { cn } from \"@/lib/utils\"\nimport { formatNumber } from \"@/registry/dashboardcn/lib/format\"\nimport {\n  ChartContainer,\n  ChartLegend,\n  ChartLegendContent,\n  ChartTooltip,\n  ChartTooltipContent,\n  type ChartConfig,\n} from \"@/components/ui/chart\"\n\nexport interface RadarSeries {\n  /** Key in each data row. */\n  key: string\n  label: string\n  /** Any CSS color. Defaults to chart-1 through chart-5 in order. */\n  color?: string\n}\n\nexport interface RadarChartProps\n  extends Omit<React.ComponentProps<typeof ChartContainer>, \"config\" | \"children\"> {\n  data: Record<string, unknown>[]\n  /** Key of the category label in each row, one per spoke. */\n  angleKey: string\n  series: RadarSeries[]\n  /** \"filled\" shades the area, \"line\" draws only the outline, \"dots\" adds a dot at every vertex. */\n  variant?: \"filled\" | \"line\" | \"dots\"\n  /** Shape of the background grid rings. */\n  grid?: \"polygon\" | \"circle\" | \"none\"\n  /** Show the category label at the end of each spoke. */\n  showAngleLabels?: boolean\n  /** Show value ticks along the vertical spoke. */\n  showRadiusAxis?: boolean\n  showLegend?: boolean\n  showTooltip?: boolean\n  /** Recharts domain for the value axis, e.g. [0, 100]. Defaults to [0, \"auto\"]. */\n  domain?: React.ComponentProps<typeof PolarRadiusAxis>[\"domain\"]\n  valueFormatter?: (value: number) => string\n  /** Animate polygons after hydration and when data changes. Respects reduced motion. */\n  animate?: boolean\n  animationDuration?: number\n}\n\nconst reducedMotionQuery = \"(prefers-reduced-motion: reduce)\"\n\nfunction subscribeToReducedMotion(onChange: () => void) {\n  const query = window.matchMedia(reducedMotionQuery)\n  query.addEventListener(\"change\", onChange)\n  return () => query.removeEventListener(\"change\", onChange)\n}\n\nfunction getReducedMotionSnapshot() {\n  return window.matchMedia(reducedMotionQuery).matches\n}\n\nfunction getReducedMotionServerSnapshot() {\n  return true\n}\n\n/** A radar chart on shadcn's chart primitives: filled, outlined, or dotted, with one polygon per series. */\nfunction RadarChart({\n  data,\n  angleKey,\n  series,\n  variant = \"filled\",\n  grid = \"polygon\",\n  showAngleLabels = true,\n  showRadiusAxis = false,\n  showLegend = false,\n  showTooltip = true,\n  domain,\n  valueFormatter = (value) => formatNumber(value, { format: \"compact\" }),\n  animate = true,\n  animationDuration = 500,\n  className,\n  ...props\n}: RadarChartProps) {\n  const reducedMotion = React.useSyncExternalStore(\n    subscribeToReducedMotion,\n    getReducedMotionSnapshot,\n    getReducedMotionServerSnapshot\n  )\n  const config = Object.fromEntries(\n    series.map((s, index) => [\n      s.key,\n      { label: s.label, color: s.color ?? `var(--chart-${(index % 5) + 1})` },\n    ])\n  ) satisfies ChartConfig\n\n  return (\n    <ChartContainer\n      data-slot=\"radar-chart\"\n      config={config}\n      className={cn(\"aspect-auto h-64 w-full\", className)}\n      {...props}\n    >\n      <RechartsRadarChart data={data} margin={{ top: 8, right: 24, bottom: 8, left: 24 }}>\n        {grid !== \"none\" ? <PolarGrid gridType={grid} /> : null}\n        <PolarAngleAxis\n          dataKey={angleKey}\n          tickLine={false}\n          axisLine={false}\n          tick={showAngleLabels ? { fill: \"var(--muted-foreground)\" } : false}\n        />\n        <PolarRadiusAxis\n          angle={90}\n          domain={domain}\n          axisLine={false}\n          tick={showRadiusAxis ? { fill: \"var(--muted-foreground)\" } : false}\n          tickFormatter={valueFormatter}\n        />\n        {showTooltip ? (\n          <ChartTooltip\n            cursor={false}\n            content={\n              <ChartTooltipContent\n                indicator=\"dot\"\n                formatter={(value, name, item) => (\n                  <div className=\"flex w-full items-center gap-2\">\n                    <span\n                      className=\"size-2 shrink-0 rounded-[2px]\"\n                      style={{ backgroundColor: item.color }}\n                    />\n                    <span className=\"text-muted-foreground flex-1\">\n                      {config[name as string]?.label ?? name}\n                    </span>\n                    <span className=\"font-mono font-medium tabular-nums\">\n                      {valueFormatter(Number(value))}\n                    </span>\n                  </div>\n                )}\n              />\n            }\n          />\n        ) : null}\n        {showLegend ? <ChartLegend content={<ChartLegendContent />} /> : null}\n        {series.map((s) => {\n          const color = `var(--color-${s.key})`\n          return (\n            <Radar\n              key={s.key}\n              dataKey={s.key}\n              stroke={color}\n              strokeWidth={2}\n              fill={color}\n              fillOpacity={variant === \"filled\" ? 0.25 : 0}\n              dot={variant === \"dots\" ? { r: 3, fill: color, strokeWidth: 0 } : false}\n              activeDot={{ r: 4, fill: color, stroke: \"var(--background)\", strokeWidth: 2 }}\n              isAnimationActive={animate && !reducedMotion}\n              animationDuration={animationDuration}\n              animationEasing=\"ease-out\"\n            />\n          )\n        })}\n      </RechartsRadarChart>\n    </ChartContainer>\n  )\n}\n\nexport { RadarChart }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/dashboardcn/lib/format.ts",
      "content": "export type NumberFormat = \"number\" | \"compact\" | \"currency\" | \"percent\"\n\nexport interface FormatNumberOptions {\n  format?: NumberFormat\n  /** ISO 4217 code, used when format is \"currency\". Defaults to USD. */\n  currency?: string\n  locale?: string\n  maximumFractionDigits?: number\n  /** Abbreviate large values, e.g. \"$158K\" instead of \"$158,143\". */\n  compact?: boolean\n}\n\nconst formatterCache = new Map<string, Intl.NumberFormat>()\n\nfunction getFormatter(locale: string, options: Intl.NumberFormatOptions) {\n  const key = locale + JSON.stringify(options)\n  let formatter = formatterCache.get(key)\n  if (!formatter) {\n    formatter = new Intl.NumberFormat(locale, options)\n    formatterCache.set(key, formatter)\n  }\n  return formatter\n}\n\n/**\n * Format a metric value for display.\n *\n * formatNumber(1234567)                          -> \"1,234,567\"\n * formatNumber(1234567, { format: \"compact\" })   -> \"1.2M\"\n * formatNumber(48.2, { format: \"currency\" })     -> \"$48.20\"\n * formatNumber(158143, { format: \"currency\", compact: true }) -> \"$158K\"\n * formatNumber(0.124, { format: \"percent\" })     -> \"12.4%\"\n */\nexport function formatNumber(\n  value: number,\n  {\n    format = \"number\",\n    currency = \"USD\",\n    locale = \"en-US\",\n    maximumFractionDigits,\n    compact = false,\n  }: FormatNumberOptions = {}\n): string {\n  if (!Number.isFinite(value)) return \"—\"\n\n  // Compact values keep three significant digits: $158K, $1.23M, 41.2K.\n  const compactOptions: Intl.NumberFormatOptions = compact\n    ? maximumFractionDigits === undefined\n      ? { notation: \"compact\", maximumSignificantDigits: 3 }\n      : { notation: \"compact\", maximumFractionDigits }\n    : {}\n  switch (format) {\n    case \"compact\":\n      return getFormatter(locale, {\n        notation: \"compact\",\n        maximumFractionDigits: maximumFractionDigits ?? 1,\n      }).format(value)\n    case \"currency\":\n      return getFormatter(locale, {\n        style: \"currency\",\n        currency,\n        maximumFractionDigits: maximumFractionDigits ?? 2,\n        ...compactOptions,\n      }).format(value)\n    case \"percent\":\n      return getFormatter(locale, {\n        style: \"percent\",\n        maximumFractionDigits: maximumFractionDigits ?? 1,\n      }).format(value)\n    default:\n      return getFormatter(locale, {\n        maximumFractionDigits: maximumFractionDigits ?? 0,\n        ...compactOptions,\n      }).format(value)\n  }\n}\n\n/**\n * Format a fractional change as a signed percentage.\n *\n * formatDelta(0.124)  -> \"+12.4%\"\n * formatDelta(-0.03)  -> \"-3.0%\"\n * formatDelta(0)      -> \"0.0%\"\n */\nexport function formatDelta(delta: number, locale = \"en-US\"): string {\n  if (!Number.isFinite(delta)) return \"—\"\n  return getFormatter(locale, {\n    style: \"percent\",\n    signDisplay: \"exceptZero\",\n    maximumFractionDigits: 1,\n    minimumFractionDigits: 1,\n  }).format(delta)\n}\n\n/**\n * Fractional change between two values. Returns 0 when there is no baseline.\n */\nexport function computeDelta(current: number, previous: number): number {\n  if (!previous) return 0\n  return (current - previous) / Math.abs(previous)\n}\n",
      "type": "registry:lib"
    }
  ],
  "type": "registry:ui"
}