{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "chart",
  "dependencies": [
    "recharts"
  ],
  "files": [
    {
      "path": "src/registry/ui/chart.tsx",
      "content": "\"use client\";\n\nimport * as RechartsPrimitive from \"recharts\";\nimport { cn } from \"@/lib/utils\";\nimport * as React from \"react\";\n\n// Format: { THEME_NAME: CSS_SELECTOR }\nconst THEMES = { light: \"\", dark: \".dark\" } as const;\n\ntype ThemeKey = keyof typeof THEMES;\n\n// All Keys are optional at first\ntype ThemeColorsBase = {\n  [K in ThemeKey]?: string[];\n};\n\n// Require at least one theme key\ntype AtLeastOneThemeColor = {\n  [K in ThemeKey]: Required<Pick<ThemeColorsBase, K>> & Partial<Omit<ThemeColorsBase, K>>;\n}[ThemeKey];\n\nconst VALID_THEME_KEYS = Object.keys(THEMES) as ThemeKey[];\n\n// Validation for chart config colors at runtime\nfunction validateChartConfigColors(config: ChartConfig): void {\n  for (const [key, value] of Object.entries(config)) {\n    if (value.colors) {\n      const hasValidThemeKey = VALID_THEME_KEYS.some(\n        (themeKey) => value.colors?.[themeKey] !== undefined,\n      );\n\n      if (!hasValidThemeKey) {\n        throw new Error(\n          `[EvilCharts] Invalid chart config for \"${key}\": colors object must have at least one theme key (${VALID_THEME_KEYS.join(\", \")}). Received empty object or invalid keys.`,\n        );\n      }\n    }\n  }\n}\n\nexport type ChartConfig = Record<\n  string,\n  {\n    label?: React.ReactNode;\n    icon?: React.ComponentType;\n    colors?: AtLeastOneThemeColor;\n  }\n>;\n\ninterface ChartContextProps {\n  config: ChartConfig;\n}\n\nconst ChartContext = React.createContext<ChartContextProps | null>(null);\n\nexport function useChart() {\n  const context = React.useContext(ChartContext);\n\n  if (!context) {\n    throw new Error(\"useChart must be used within a <ChartContainer />\");\n  }\n\n  return context;\n}\n\ninterface ChartContainerProps\n  extends\n    Omit<React.ComponentProps<\"div\">, \"children\">,\n    Pick<\n      React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>,\n      | \"initialDimension\"\n      | \"aspect\"\n      | \"debounce\"\n      | \"minHeight\"\n      | \"minWidth\"\n      | \"maxHeight\"\n      | \"height\"\n      | \"width\"\n      | \"onResize\"\n      | \"children\"\n    > {\n  config: ChartConfig;\n  innerResponsiveContainerStyle?: React.ComponentProps<\n    typeof RechartsPrimitive.ResponsiveContainer\n  >[\"style\"];\n  /** Optional content rendered below the chart (e.g. EvilBrush) */\n  footer?: React.ReactNode;\n}\n\nfunction ChartContainer({\n  id,\n  config,\n  initialDimension = { width: 320, height: 200 },\n  className,\n  children,\n  footer,\n  ...props\n}: Readonly<ChartContainerProps>) {\n  const uniqueId = React.useId();\n  const chartId = `chart-${id ?? uniqueId.replace(/:/g, \"\")}`;\n\n  // Validate chart config at runtime\n  validateChartConfigColors(config);\n\n  return (\n    <ChartContext.Provider value={{ config }}>\n      <div\n        data-slot=\"chart\"\n        data-chart={chartId}\n        className={cn(\n          \"min-h-0 w-full flex-1\",\n          \"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border relative flex flex-col justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden\",\n          !footer && \"aspect-video\",\n          className,\n        )}\n        {...props}\n      >\n        <ChartStyle id={chartId} config={config} />\n        <RechartsPrimitive.ResponsiveContainer\n          className=\"min-h-0 w-full flex-1\"\n          initialDimension={initialDimension}\n        >\n          {children}\n        </RechartsPrimitive.ResponsiveContainer>\n        {footer}\n      </div>\n    </ChartContext.Provider>\n  );\n}\n\nfunction LoadingIndicator({ isLoading }: { isLoading: boolean }) {\n  if (!isLoading) {\n    return null;\n  }\n\n  return (\n    <div className=\"pointer-events-none absolute inset-0 z-20 flex items-center justify-center\">\n      <div className=\"text-primary bg-background flex items-center justify-center gap-2 rounded-md border px-2 py-0.5 text-sm\">\n        <div className=\"border-border border-t-primary h-3 w-3 animate-spin rounded-full border\" />\n        <span>Loading</span>\n      </div>\n    </div>\n  );\n}\n\n// Distribute colors evenly across slots, extra slots go to last color(s)\n// Example: 2 colors for 4 slots → [red, red, pink, pink]\n// Example: 3 colors for 4 slots → [red, pink, blue, blue]\nfunction distributeColors(colorsArray: string[], maxCount: number): string[] {\n  const availableCount = colorsArray.length;\n  if (availableCount >= maxCount) {\n    return colorsArray.slice(0, maxCount);\n  }\n\n  const result: string[] = [];\n  const baseSlots = Math.floor(maxCount / availableCount);\n  const extraSlots = maxCount % availableCount;\n\n  // First (availableCount - extraSlots) colors get baseSlots each\n  // Last extraSlots colors get (baseSlots + 1) each\n  for (let colorIdx = 0; colorIdx < availableCount; colorIdx++) {\n    const isExtraColor = colorIdx >= availableCount - extraSlots;\n    const slotsForThisColor = baseSlots + (isExtraColor ? 1 : 0);\n    for (let j = 0; j < slotsForThisColor; j++) {\n      result.push(colorsArray[colorIdx]);\n    }\n  }\n\n  return result;\n}\n\nconst ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {\n  const colorConfig = Object.entries(config).filter(([, config]) => config.colors);\n\n  if (!colorConfig.length) {\n    return null;\n  }\n\n  const generateCssVars = (theme: keyof typeof THEMES) =>\n    colorConfig\n      .flatMap(([key, itemConfig]) => {\n        const colorsArray = itemConfig.colors?.[theme];\n        if (!colorsArray || !Array.isArray(colorsArray) || colorsArray.length === 0) {\n          return [];\n        }\n\n        // Get max count across all themes for this key\n        const maxCount = getColorsCount(itemConfig);\n\n        // Distribute colors evenly across all required slots\n        const distributedColors = distributeColors(colorsArray, maxCount);\n\n        return distributedColors.map((color, index) => `  --color-${key}-${index}: ${color};`);\n      })\n      .filter(Boolean)\n      .join(\"\\n\");\n\n  const css = Object.entries(THEMES)\n    .map(\n      ([theme, prefix]) =>\n        `${prefix} [data-chart=${id}] {\\n${generateCssVars(theme as keyof typeof THEMES)}\\n}`,\n    )\n    .join(\"\\n\");\n\n  return <style dangerouslySetInnerHTML={{ __html: css }} />;\n};\n\n// Helper to extract item config from a payload.\nexport function getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: string) {\n  if (typeof payload !== \"object\" || payload === null) {\n    return undefined;\n  }\n\n  const payloadPayload =\n    \"payload\" in payload && typeof payload.payload === \"object\" && payload.payload !== null\n      ? payload.payload\n      : undefined;\n\n  let configLabelKey: string = key;\n\n  if (key in payload && typeof payload[key as keyof typeof payload] === \"string\") {\n    configLabelKey = payload[key as keyof typeof payload] as string;\n  } else if (\n    payloadPayload &&\n    key in payloadPayload &&\n    typeof payloadPayload[key as keyof typeof payloadPayload] === \"string\"\n  ) {\n    configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string;\n  }\n\n  return configLabelKey in config ? config[configLabelKey] : config[key];\n}\n\n// Format values to percent for expanded charts\nfunction axisValueToPercentFormatter(value: number) {\n  return `${Math.round(value * 100).toFixed(0)}%`;\n}\n\n// Get max colors count across all themes for a config entry\nfunction getColorsCount(config: ChartConfig[string]): number {\n  if (!config.colors) return 1;\n  const counts = VALID_THEME_KEYS.map((theme) => config.colors?.[theme]?.length ?? 0);\n  return Math.max(...counts, 1);\n}\n\n// Generate random loading data for skeleton/loading state\n// min/max represent percentage of the range (0-100), defaults to 20-80 for realistic look\nexport const getLoadingData = (points: number = 10, min: number = 0, max: number = 70) => {\n  const range = max - min;\n  return Array.from({ length: points }, () => ({\n    loading: Math.floor(Math.random() * range) + min,\n  }));\n};\n\nexport {\n  ChartContainer,\n  ChartStyle,\n  axisValueToPercentFormatter,\n  LoadingIndicator,\n  getColorsCount,\n};\n",
      "type": "registry:component",
      "target": "components/evilcharts/ui/chart.tsx"
    }
  ],
  "type": "registry:component"
}