{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "recharts-radial-chart",
  "description": "Radial bar chart component with full and semi-circle variants",
  "dependencies": [
    "recharts",
    "motion"
  ],
  "registryDependencies": [
    "@evilcharts/recharts-chart",
    "@evilcharts/recharts-tooltip",
    "@evilcharts/recharts-legend",
    "@evilcharts/recharts-background"
  ],
  "files": [
    {
      "path": "src/registry/charts/recharts-radial-chart.tsx",
      "content": "\"use client\";\n\nimport {\n  PolarAngleAxis,\n  RadialBar as RechartsRadialBar,\n  RadialBarChart as RechartsRadialBarChart,\n  Sector,\n  type SectorProps,\n} from \"recharts\";\nimport {\n  createContext,\n  use,\n  useCallback,\n  useEffect,\n  useId,\n  useMemo,\n  useState,\n  type ComponentProps,\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 {\n  ChartLegend,\n  ChartLegendContent,\n  type ChartLegendVariant,\n} from \"@/registry/ui/recharts-legend\";\nimport { ChartBackground, type BackgroundVariant } from \"@/registry/ui/recharts-background\";\nimport { TypedDataKey } from \"recharts/types/util/typedDataKey\";\n\n// Constants\nconst DEFAULT_INNER_RADIUS = \"30%\";\nconst DEFAULT_OUTER_RADIUS = \"100%\";\nconst DEFAULT_CORNER_RADIUS = 5;\nconst DEFAULT_BAR_SIZE = 14;\nconst LOADING_BARS = 5;\nconst LOADING_ANIMATION_DURATION = 1500; // in milliseconds — interval between skeleton data changes\n\ntype RadialBarChartProps = ComponentProps<typeof RechartsRadialBarChart>;\ntype RadialBarRechartsProps = ComponentProps<typeof RechartsRadialBar>;\n\ntype RadialVariant = \"full\" | \"semi\";\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Shared context\n// ─────────────────────────────────────────────────────────────────────────────\n\n/**\n * Shared state for every part of the chart. Lifted into <EvilRadialChart /> so\n * that <RadialBar />, <Tooltip />, and <Legend /> can read it without prop\n * drilling. Sub-components are composed freely — the provider is the single\n * source of truth.\n */\ntype RadialChartContextValue = {\n  config: ChartConfig; // colors + labels for every bar\n  nameKey: string; // data key holding each bar's name\n  chartId: string; // colon-free id scoping this chart's style defs\n  isLoading: boolean; // whether the chart shows its loading skeleton\n  selectedBar: string | null; // currently selected bar name, or null when none\n  selectBar: (barName: string | null, value?: number) => void; // sets the selected bar\n};\n\nconst RadialChartContext = createContext<RadialChartContextValue | null>(null);\n\n// Reads the chart context, throwing a helpful error when used outside <EvilRadialChart />\nfunction useRadialChart() {\n  const context = use(RadialChartContext);\n\n  if (!context) {\n    throw new Error(\n      \"Radial chart parts (<RadialBar />, <Tooltip />, …) must be used within <EvilRadialChart />\",\n    );\n  }\n\n  return context;\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Root container\n// ─────────────────────────────────────────────────────────────────────────────\n\ntype EvilRadialChartBaseProps<TData extends Record<string, unknown>> = {\n  config: ChartConfig; // bar colors + labels, keyed by each bar's name\n  data: TData[]; // rows rendered by the chart — one bar per row\n  nameKey: keyof TData & string; // data key holding each bar's name\n  children: ReactNode; // composed parts — <RadialBar />, <Tooltip />, <Legend />\n  className?: string; // extra classes for the chart container\n  chartProps?: RadialBarChartProps; // escape hatch for the raw Recharts chart\n  variant?: RadialVariant; // arc shape — full circle or half circle\n  // Value a full sweep represents. Without it the scale is derived from the data,\n  // so the largest bar always fills the arc — set it (e.g. 100) for gauges, where\n  // a single value has to read against a fixed total.\n  max?: number;\n  innerRadius?: number | string; // inner radius of the radial bars\n  outerRadius?: number | string; // outer radius of the radial bars\n  defaultSelectedDataKey?: string | null; // bar selected on first render\n  onSelectionChange?: (selection: { dataKey: string; value: number } | null) => void; // fires when the selected bar changes\n  isLoading?: boolean; // shows the animated loading skeleton\n  backgroundVariant?: BackgroundVariant; // background pattern behind the chart\n};\n\ntype EvilRadialChartProps<TData extends Record<string, unknown>> = EvilRadialChartBaseProps<TData>;\n\n/**\n * Root of the composible radial chart. Owns the data, the shared context, the\n * loading skeleton, and the chart-wide arc shape. Everything visual — the\n * tooltip, legend, and the radial bar itself — is composed as children, so a\n * consumer renders exactly the parts they need.\n */\nexport function EvilRadialChart<TData extends Record<string, unknown>>({\n  config,\n  data,\n  nameKey,\n  children,\n  className,\n  chartProps,\n  variant = \"full\",\n  max,\n  innerRadius = DEFAULT_INNER_RADIUS,\n  outerRadius = DEFAULT_OUTER_RADIUS,\n  defaultSelectedDataKey = null,\n  onSelectionChange,\n  isLoading = false,\n  backgroundVariant,\n}: EvilRadialChartProps<TData>) {\n  const chartId = useId().replace(/:/g, \"\"); // colon-free id keeps CSS/SVG selectors valid\n  const [selectedBar, setSelectedBar] = useState<string | null>(defaultSelectedDataKey);\n  const loadingData = useLoadingData(isLoading);\n\n  const variantConfig = getVariantConfig(variant);\n\n  // Updates selection state and notifies the parent with the bar's value\n  const selectBar = useCallback(\n    (barName: string | null, value?: number) => {\n      setSelectedBar(barName);\n      onSelectionChange?.(barName === null ? null : { dataKey: barName, value: value ?? 0 });\n    },\n    [onSelectionChange],\n  );\n\n  // Real bars paint from a per-name gradient; the skeleton keeps the raw rows\n  const preparedData = useMemo(\n    () =>\n      data.map((item) => ({\n        ...item,\n        fill: `url(#${chartId}-radial-colors-${item[nameKey] as string})`,\n      })),\n    [data, nameKey, chartId],\n  );\n\n  const contextValue = useMemo<RadialChartContextValue>(\n    () => ({\n      config,\n      nameKey,\n      chartId,\n      isLoading,\n      selectedBar,\n      selectBar,\n    }),\n    [config, nameKey, chartId, isLoading, selectedBar, selectBar],\n  );\n\n  return (\n    <RadialChartContext value={contextValue}>\n      <ChartContainer className={className} config={config}>\n        <LoadingIndicator isLoading={isLoading} />\n        <RechartsRadialBarChart\n          id={chartId}\n          data={isLoading ? loadingData : preparedData}\n          innerRadius={innerRadius}\n          outerRadius={outerRadius}\n          startAngle={variantConfig.startAngle}\n          endAngle={variantConfig.endAngle}\n          cx={variantConfig.cx}\n          cy={variantConfig.cy}\n          {...chartProps}\n        >\n          {/* Pinning the angle domain is what lets a single value read against a\n              fixed total instead of auto-scaling to fill the arc. */}\n          {max != null && max > 0 && (\n            <PolarAngleAxis type=\"number\" domain={[0, max]} tick={false} axisLine={false} />\n          )}\n          {backgroundVariant && <ChartBackground variant={backgroundVariant} />}\n          {children}\n          {isLoading && <LoadingRadialBar />}\n          <defs>\n            <ColorGradientStyle config={config} chartId={chartId} />\n          </defs>\n        </RechartsRadialBarChart>\n      </ChartContainer>\n    </RadialChartContext>\n  );\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Composible parts\n// ─────────────────────────────────────────────────────────────────────────────\n\ntype RadialBarProps = {\n  dataKey: string; // value key — determines each bar's size\n  cornerRadius?: number; // border radius of each bar's corners\n  barSize?: number; // thickness of each radial bar\n  showBackground?: boolean; // renders the unfilled track behind each bar\n  isClickable?: boolean; // lets bars be selected by clicking them\n  radialBarProps?: Omit<RadialBarRechartsProps, \"dataKey\">; // escape hatch for raw Recharts RadialBar props\n};\n\n/**\n * The radial bar series. Each data row becomes one bar. Pass `isClickable` to\n * make bars selectable.\n */\nfunction RadialBar({\n  dataKey,\n  cornerRadius = DEFAULT_CORNER_RADIUS,\n  barSize = DEFAULT_BAR_SIZE,\n  showBackground = true,\n  isClickable = false,\n  radialBarProps,\n}: RadialBarProps) {\n  const { nameKey, isLoading, selectedBar, selectBar } = useRadialChart();\n\n  // The root renders the skeleton bar while loading, so the real bar steps aside\n  if (isLoading) return null;\n\n  return (\n    <RechartsRadialBar\n      dataKey={dataKey as TypedDataKey<Record<string, unknown>>}\n      cornerRadius={cornerRadius}\n      barSize={barSize}\n      background={showBackground}\n      className=\"drop-shadow-sm\"\n      style={isClickable ? { cursor: \"pointer\" } : undefined}\n      onClick={(payload, index) => {\n        if (!isClickable) return;\n        const entry = payload as Record<string, unknown>;\n        const barName = (entry?.[nameKey] as string | undefined) ?? String(index);\n        const value = Number(entry?.[dataKey] ?? 0);\n        // Clicking the selected bar clears the selection, otherwise selects it\n        selectBar(selectedBar === barName ? null : barName, value);\n      }}\n      shape={(props: SectorProps) => {\n        const barName = (props as unknown as Record<string, unknown>)[nameKey] as string;\n        const isSelected = selectedBar === null || selectedBar === barName;\n\n        return (\n          <Sector\n            {...props}\n            opacity={isClickable && !isSelected ? 0.15 : 1}\n            className=\"transition-opacity duration-200\"\n          />\n        );\n      }}\n      {...radialBarProps}\n    />\n  );\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. Labels each bar by its name from context. Hidden\n * automatically while the chart is loading.\n */\nfunction Tooltip({ variant, roundness, defaultIndex }: TooltipProps) {\n  const { nameKey, isLoading } = useRadialChart();\n\n  if (isLoading) return null;\n\n  return (\n    <ChartTooltip\n      defaultIndex={defaultIndex}\n      cursor={false}\n      content={\n        <ChartTooltipContent nameKey={nameKey} hideLabel 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 bar\n};\n\n/**\n * The bar legend. When `isClickable` is set, each entry toggles selection of\n * its bar, driving the shared selection state read by <RadialBar />. Hidden\n * automatically while the chart is loading.\n */\nfunction Legend({\n  variant,\n  align = \"center\",\n  verticalAlign = \"bottom\",\n  isClickable = false,\n}: LegendProps) {\n  const { nameKey, isLoading, selectedBar, selectBar } = useRadialChart();\n\n  if (isLoading) return null;\n\n  return (\n    <ChartLegend\n      verticalAlign={verticalAlign}\n      align={align}\n      content={\n        <ChartLegendContent\n          selected={selectedBar}\n          onSelectChange={selectBar}\n          isClickable={isClickable}\n          nameKey={nameKey}\n          variant={variant}\n        />\n      }\n    />\n  );\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Variant helpers\n// ─────────────────────────────────────────────────────────────────────────────\n\n// Returns the angle + center configuration for the chart's arc shape\nfunction getVariantConfig(variant: RadialVariant) {\n  switch (variant) {\n    case \"semi\":\n      return { startAngle: 180, endAngle: 0, cx: \"50%\", cy: \"70%\" };\n    case \"full\":\n    default:\n      return { startAngle: 90, endAngle: -270, cx: \"50%\", cy: \"50%\" };\n  }\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Style definitions — scoped to the chart's unique id\n// ─────────────────────────────────────────────────────────────────────────────\n\n/** Diagonal color gradient applied to every radial bar, one per config key. */\nconst ColorGradientStyle = ({ config, chartId }: { config: ChartConfig; chartId: string }) => {\n  return (\n    <>\n      {Object.entries(config).map(([dataKey, colorConfig]) => {\n        const colorsCount = getColorsCount(colorConfig);\n\n        return (\n          <linearGradient\n            key={`${chartId}-radial-colors-${dataKey}`}\n            id={`${chartId}-radial-colors-${dataKey}`}\n            x1=\"0\"\n            y1=\"0\"\n            x2=\"1\"\n            y2=\"1\"\n          >\n            {colorsCount === 1 ? (\n              <>\n                <stop offset=\"0%\" stopColor={`var(--color-${dataKey}-0)`} />\n                <stop offset=\"100%\" stopColor={`var(--color-${dataKey}-0)`} />\n              </>\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                  />\n                );\n              })\n            )}\n          </linearGradient>\n        );\n      })}\n    </>\n  );\n};\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Loading skeleton\n// ─────────────────────────────────────────────────────────────────────────────\n\n// Builds random skeleton rows with values between 40 and 100\nfunction generateLoadingData() {\n  return Array.from({ length: LOADING_BARS }, (_, i) => ({\n    name: `loading${i}`,\n    value: 40 + Math.random() * 60,\n  }));\n}\n\n// Hook to animate the loading skeleton data at fixed intervals\nfunction useLoadingData(isLoading: boolean) {\n  const [tick, setTick] = useState(0);\n\n  useEffect(() => {\n    if (!isLoading) return;\n\n    const interval = setInterval(() => {\n      setTick((prev) => prev + 1);\n    }, LOADING_ANIMATION_DURATION);\n\n    return () => clearInterval(interval);\n  }, [isLoading]);\n\n  // Regenerate skeleton data whenever the interval ticks\n  // eslint-disable-next-line react-hooks/exhaustive-deps\n  const loadingData = useMemo(() => generateLoadingData(), [tick]);\n\n  return loadingData;\n}\n\n/**\n * The skeleton bar shown while the chart is loading. Rendered by the root in\n * place of the real <RadialBar />, with animated values and a muted fill.\n */\nconst LoadingRadialBar = () => {\n  return (\n    <RechartsRadialBar\n      dataKey=\"value\"\n      cornerRadius={DEFAULT_CORNER_RADIUS}\n      barSize={DEFAULT_BAR_SIZE}\n      background\n      isAnimationActive\n      animationDuration={LOADING_ANIMATION_DURATION}\n      animationEasing=\"ease-in-out\"\n      shape={(props: SectorProps) => <Sector {...props} fill=\"currentColor\" fillOpacity={0.25} />}\n    />\n  );\n};\n\n// Compound API: every part hangs off the root as a static member, so a consumer\n// writes <EvilRadialChart.RadialBar/>, <EvilRadialChart.Tooltip/>, … from a single\n// import — no colliding named marker exports when several charts share one file.\nEvilRadialChart.RadialBar = RadialBar;\nEvilRadialChart.Tooltip = Tooltip;\nEvilRadialChart.Legend = Legend;\n",
      "type": "registry:component",
      "target": "components/evilcharts/charts/recharts-radial-chart.tsx"
    }
  ],
  "type": "registry:component"
}