{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "chart",
  "type": "registry:ui",
  "title": "Chart",
  "description": "Composable Recharts wrapper with themed tooltips, legends, and design-system color tokens.",
  "dependencies": [
    "@saasflare/ui",
    "recharts"
  ],
  "files": [
    {
      "path": "components/ui/chart.tsx",
      "type": "registry:ui",
      "content": "// @toreview\n\"use client\"\n\n/**\n * @fileoverview Chart primitive — themed chart container and tooltip/legend components for data visualization.\n * Built on Recharts. Provides a ChartConfig-driven theming system with\n * light/dark mode support, custom tooltips, and accessible legends.\n * Part of the Saasflare base component layer.\n * @module packages/ui/components/ui/chart\n * @layer core\n *\n * @requires recharts — peer dependency. Shipped via the `/chart` subpath:\n *   `import { ChartContainer } from \"@saasflare/ui/chart\"`.\n *\n * @component\n * @example\n * import { ChartContainer, ChartTooltip, ChartTooltipContent } from '@saasflare/ui/chart';\n * const config = { revenue: { label: \"Revenue\", color: \"var(--chart-1)\" } };\n * <ChartContainer config={config}>\n *   <BarChart data={data}>\n *     <Bar dataKey=\"revenue\" />\n *     <ChartTooltip content={<ChartTooltipContent />} />\n *   </BarChart>\n * </ChartContainer>\n */\n\nimport * as React from \"react\"\nimport * as RechartsPrimitive from \"recharts\"\n\nimport { cn } from \"@saasflare/ui\"\nimport { useSaasflareProps, type SaasflareComponentProps } from \"@saasflare/ui\"\n\n// Format: { THEME_NAME: CSS_SELECTOR }\nconst THEMES = { light: \"\", dark: \".dark\" } as const\n\n/**\n * Per-series chart configuration keyed by data key. Each entry supplies an\n * optional `label` and `icon` plus either a static `color` or a per-theme\n * (`light`/`dark`) color map. Consumed by {@link ChartContainer}, which exposes\n * each color as a `--color-{key}` CSS variable, and read by the tooltip and\n * legend content components.\n */\nexport type ChartConfig = {\n  [k in string]: {\n    label?: React.ReactNode\n    icon?: React.ComponentType\n  } & (\n    | { color?: string; theme?: never }\n    | { color?: never; theme: Record<keyof typeof THEMES, string> }\n  )\n}\n\ntype ChartContextProps = {\n  config: ChartConfig\n}\n\nconst ChartContext = React.createContext<ChartContextProps | null>(null)\n\nfunction 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\n/**\n * Props for {@link ChartContainer}. `config` drives series colors, labels, and\n * icons; `children` must satisfy Recharts' `ResponsiveContainer` child\n * contract (a single chart element).\n */\ninterface ChartContainerProps\n  extends Omit<React.ComponentProps<\"div\">, keyof SaasflareComponentProps>,\n    SaasflareComponentProps {\n  config: ChartConfig\n  children: React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>[\"children\"]\n}\n\n/**\n * Themed wrapper for Recharts charts. Provides the {@link ChartConfig} to\n * descendant tooltip/legend components via context, injects per-series\n * `--color-{key}` CSS variables (light/dark aware), and renders children in a\n * responsive 16:9 container with Saasflare token styling applied to Recharts\n * internals. Required ancestor for {@link ChartTooltipContent} and\n * {@link ChartLegendContent}.\n *\n * @component\n * @layer core\n */\nfunction ChartContainer({\n  id,\n  className,\n  children,\n  config,\n  surface,\n  radius,\n  animated,\n  iconWeight,\n  ...props\n}: ChartContainerProps) {\n  const sf = useSaasflareProps({ surface, radius, animated, iconWeight })\n  const uniqueId = React.useId()\n  const chartId = `chart-${id || uniqueId.replace(/:/g, \"\")}`\n\n  return (\n    <ChartContext.Provider value={{ config }}>\n      <div\n        data-surface={sf.surface}\n        data-radius={sf.radius}\n        data-animated={String(sf.animated)}\n        data-slot=\"chart\"\n        data-chart={chartId}\n        className={cn(\n          \"flex aspect-video justify-center text-xs [&_.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-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.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 [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden\",\n          className\n        )}\n        {...props}\n      >\n        <ChartStyle id={chartId} config={config} />\n        <RechartsPrimitive.ResponsiveContainer>\n          {children}\n        </RechartsPrimitive.ResponsiveContainer>\n      </div>\n    </ChartContext.Provider>\n  )\n}\n\n/**\n * ChartStyle — injects per-chart CSS custom properties (`--color-{key}`)\n * derived from the {@link ChartConfig}, scoped by `[data-chart={id}]` and\n * theme prefix so light/dark color overrides resolve correctly.\n *\n * @component\n */\nconst ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {\n  const colorConfig = Object.entries(config).filter(\n    ([, config]) => config.theme || config.color\n  )\n\n  if (!colorConfig.length) {\n    return null\n  }\n\n  return (\n    <style\n      dangerouslySetInnerHTML={{\n        __html: Object.entries(THEMES)\n          .map(\n            ([theme, prefix]) => `\n${prefix} [data-chart=${id}] {\n${colorConfig\n  .map(([key, itemConfig]) => {\n    const color =\n      itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||\n      itemConfig.color\n    return color ? `  --color-${key}: ${color};` : null\n  })\n  .join(\"\\n\")}\n}\n`\n          )\n          .join(\"\\n\"),\n      }}\n    />\n  )\n}\n\n/**\n * Re-export of the Recharts `Tooltip` component. Place inside a chart and pass\n * {@link ChartTooltipContent} via `content` to get the themed, config-driven\n * tooltip body.\n *\n * @component\n */\nconst ChartTooltip = RechartsPrimitive.Tooltip\n\ninterface ChartTooltipPayloadItem {\n  name?: string\n  value?: number | string\n  dataKey?: string | number\n  type?: string\n  color?: string\n  fill?: string\n  payload?: Record<string, unknown>\n}\n\ninterface ChartTooltipContentProps\n  extends Omit<React.ComponentProps<\"div\">, keyof SaasflareComponentProps>,\n    SaasflareComponentProps {\n  active?: boolean\n  payload?: ChartTooltipPayloadItem[]\n  label?: string | number\n  labelFormatter?: (label: unknown, payload: ChartTooltipPayloadItem[]) => React.ReactNode\n  labelClassName?: string\n  formatter?: (\n    value: unknown,\n    name: string,\n    entry: ChartTooltipPayloadItem,\n    index: number,\n    payload: unknown\n  ) => React.ReactNode\n  color?: string\n  hideLabel?: boolean\n  hideIndicator?: boolean\n  indicator?: \"line\" | \"dot\" | \"dashed\"\n  nameKey?: string\n  labelKey?: string\n}\n\n/**\n * ChartTooltipContent — themed tooltip body for Recharts charts. Renders the\n * label, per-series indicator chips (or config-supplied icons), and formatted\n * values driven by the surrounding {@link ChartConfig}.\n *\n * @component\n */\nfunction ChartTooltipContent({\n  active,\n  payload,\n  className,\n  indicator = \"dot\",\n  hideLabel = false,\n  hideIndicator = false,\n  label,\n  labelFormatter,\n  labelClassName,\n  formatter,\n  color,\n  nameKey,\n  labelKey,\n  surface,\n  radius,\n  animated,\n  iconWeight,\n}: ChartTooltipContentProps) {\n  const { config } = useChart()\n  const sf = useSaasflareProps({ surface, radius, animated, iconWeight })\n\n  const tooltipLabel = React.useMemo(() => {\n    if (hideLabel || !payload?.length) {\n      return null\n    }\n\n    const [item] = payload\n    const key = `${labelKey || item?.dataKey || item?.name || \"value\"}`\n    const itemConfig = getPayloadConfigFromPayload(config, item, key)\n    const value =\n      !labelKey && typeof label === \"string\"\n        ? config[label as keyof typeof config]?.label || label\n        : itemConfig?.label\n\n    if (labelFormatter) {\n      return (\n        <div className={cn(\"font-medium\", labelClassName)}>\n          {labelFormatter(value, payload)}\n        </div>\n      )\n    }\n\n    if (!value) {\n      return null\n    }\n\n    return <div className={cn(\"font-medium\", labelClassName)}>{value}</div>\n  }, [\n    label,\n    labelFormatter,\n    payload,\n    hideLabel,\n    labelClassName,\n    config,\n    labelKey,\n  ])\n\n  if (!active || !payload?.length) {\n    return null\n  }\n\n  const nestLabel = payload.length === 1 && indicator !== \"dot\"\n\n  return (\n    <div\n      data-surface={sf.surface}\n      data-radius={sf.radius}\n      data-animated={String(sf.animated)}\n      data-slot=\"chart-tooltip-content\"\n      className={cn(\n        \"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl\",\n        className\n      )}\n    >\n      {!nestLabel ? tooltipLabel : null}\n      <div className=\"grid gap-1.5\">\n        {payload\n          .filter((item) => item.type !== \"none\")\n          .map((item, index) => {\n            const key = `${nameKey || item.name || item.dataKey || \"value\"}`\n            const itemConfig = getPayloadConfigFromPayload(config, item, key)\n            const indicatorColor = color || item.payload?.fill || item.color\n\n            return (\n              <div\n                key={`${key}-${index}`}\n                className={cn(\n                  \"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground\",\n                  indicator === \"dot\" && \"items-center\"\n                )}\n              >\n                {formatter && item?.value !== undefined && item.name ? (\n                  formatter(item.value, item.name, item, index, item.payload)\n                ) : (\n                  <>\n                    {itemConfig?.icon ? (\n                      <itemConfig.icon />\n                    ) : (\n                      !hideIndicator && (\n                        <div\n                          className={cn(\n                            \"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)\",\n                            {\n                              \"h-2.5 w-2.5\": indicator === \"dot\",\n                              \"w-1\": indicator === \"line\",\n                              \"w-0 border-[1.5px] border-dashed bg-transparent\":\n                                indicator === \"dashed\",\n                              \"my-0.5\": nestLabel && indicator === \"dashed\",\n                            }\n                          )}\n                          style={\n                            {\n                              \"--color-bg\": indicatorColor,\n                              \"--color-border\": indicatorColor,\n                            } as React.CSSProperties\n                          }\n                        />\n                      )\n                    )}\n                    <div\n                      className={cn(\n                        \"flex flex-1 justify-between leading-none\",\n                        nestLabel ? \"items-end\" : \"items-center\"\n                      )}\n                    >\n                      <div className=\"grid gap-1.5\">\n                        {nestLabel ? tooltipLabel : null}\n                        <span className=\"text-muted-foreground\">\n                          {itemConfig?.label || item.name}\n                        </span>\n                      </div>\n                      {item.value !== undefined && item.value !== null && (\n                        <span className=\"font-mono font-medium text-foreground tabular-nums\">\n                          {item.value.toLocaleString()}\n                        </span>\n                      )}\n                    </div>\n                  </>\n                )}\n              </div>\n            )\n          })}\n      </div>\n    </div>\n  )\n}\n\n/**\n * Re-export of the Recharts `Legend` component. Place inside a chart and pass\n * {@link ChartLegendContent} via `content` to get the themed, config-driven\n * legend body.\n *\n * @component\n */\nconst ChartLegend = RechartsPrimitive.Legend\n\ninterface ChartLegendPayloadItem {\n  value?: string\n  type?: string\n  color?: string\n  dataKey?: string | number\n}\n\ninterface ChartLegendContentProps\n  extends Omit<React.ComponentProps<\"div\">, keyof SaasflareComponentProps>,\n    SaasflareComponentProps {\n  payload?: ChartLegendPayloadItem[]\n  verticalAlign?: \"top\" | \"bottom\" | \"middle\"\n  hideIcon?: boolean\n  nameKey?: string\n}\n\n/**\n * ChartLegendContent — themed legend body for Recharts charts. Renders each\n * series with its config-supplied icon (or a color swatch) and label, driven\n * by the surrounding {@link ChartConfig}.\n *\n * @component\n */\nfunction ChartLegendContent({\n  className,\n  hideIcon = false,\n  payload,\n  verticalAlign = \"bottom\",\n  nameKey,\n  surface,\n  radius,\n  animated,\n  iconWeight,\n}: ChartLegendContentProps) {\n  const { config } = useChart()\n  const sf = useSaasflareProps({ surface, radius, animated, iconWeight })\n\n  if (!payload?.length) {\n    return null\n  }\n\n  return (\n    <div\n      data-surface={sf.surface}\n      data-radius={sf.radius}\n      data-animated={String(sf.animated)}\n      data-slot=\"chart-legend-content\"\n      className={cn(\n        \"flex items-center justify-center gap-4\",\n        verticalAlign === \"top\" ? \"pb-3\" : \"pt-3\",\n        className\n      )}\n    >\n      {payload\n        .filter((item) => item.type !== \"none\")\n        .map((item, index) => {\n          const key = `${nameKey || item.dataKey || \"value\"}`\n          const itemConfig = getPayloadConfigFromPayload(config, item, key)\n\n          return (\n            <div\n              key={`${key}-${index}`}\n              className={cn(\n                \"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground\"\n              )}\n            >\n              {itemConfig?.icon && !hideIcon ? (\n                <itemConfig.icon />\n              ) : (\n                <div\n                  className=\"h-2 w-2 shrink-0 rounded-[2px]\"\n                  style={{\n                    backgroundColor: item.color,\n                  }}\n                />\n              )}\n              {itemConfig?.label}\n            </div>\n          )\n        })}\n    </div>\n  )\n}\n\n// Helper to extract item config from a payload.\nfunction getPayloadConfigFromPayload(\n  config: ChartConfig,\n  payload: unknown,\n  key: string\n) {\n  if (typeof payload !== \"object\" || payload === null) {\n    return undefined\n  }\n\n  const payloadPayload =\n    \"payload\" in payload &&\n    typeof payload.payload === \"object\" &&\n    payload.payload !== null\n      ? payload.payload\n      : undefined\n\n  let configLabelKey: string = key\n\n  if (\n    key in payload &&\n    typeof payload[key as keyof typeof payload] === \"string\"\n  ) {\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[\n      key as keyof typeof payloadPayload\n    ] as string\n  }\n\n  return configLabelKey in config\n    ? config[configLabelKey]\n    : config[key as keyof typeof config]\n}\n\nexport {\n  ChartContainer,\n  ChartTooltip,\n  ChartTooltipContent,\n  ChartLegend,\n  ChartLegendContent,\n  ChartStyle,\n  type ChartContainerProps,\n}\n",
      "target": "components/ui/chart.tsx"
    }
  ],
  "$meta": {
    "source": "https://ui.saasflare.io/r/chart.json"
  }
}
