# Chart (shadcn API) (Lumen Halo): prompt.md (v1.0.0)

- id: `lumen-chart-shadcn` · version 1.0.0 · component · free
- category: Charts
- build: Base UI (one set of files; its dependencies follow the build)
- install (this build): `npx shadcn@latest add https://beautiful-ui.dev/r/lumen-chart-shadcn.json`
- npm dependencies: recharts@^3
- registry dependencies: utils, @beautiful-ui/lumen-chart, https://beautiful-ui.dev/r/lumen-foundation.json
- docs: https://beautiful-ui.dev/components/lumen-chart-shadcn
- The install command carries everything this item needs (files, CSS, tokens, npm and registry dependencies). Prefer it to copying source by hand.

shadcn's chart API, exactly, drawn in Lumen: ChartContainer, ChartTooltip, ChartTooltipContent, ChartLegend, ChartLegendContent, ChartStyle and useChart, with shadcn's ChartConfig (React-node labels and icons). Every example on ui.shadcn.com/charts works by changing one import.

## Build it
- Stack: React 19 (`ref` is a plain prop), TypeScript, Tailwind CSS v4 utilities, a shadcn-initialised project with the `@/*` alias.
- Packages: `recharts@^3`.
- Files: `components/ui/lumen/charts/chart-shadcn.tsx`.
- Registry dependencies, installed with it automatically: shadcn `utils` (cn), `lumen-chart`, `lumen-foundation`.
- Builds: one set of files for both, but its dependencies come in Base UI and Radix builds. Install the one that matches the project (see Install): a free item's bare URL installs the Base UI build of it and its dependencies.
- Exports to keep: `ChartContainer`, `ChartTooltipContent`, `ChartLegendContent`, and every exported type.
- CSS: the install merges this item's rules (the registry `css` field) into your global stylesheet, in `@layer components`, and adds the lumen foundation (tokens, keyframes, motion levels) once. Nothing to import by hand.
- Re-running `add` (or `--overwrite`) re-applies those rules: put overrides in your own CSS, never in the installed rules.
- Tokens: retheme with the `--lumen-*` custom properties (`--lumen-accent`, `--lumen-accent-text`, `--lumen-bad`, `--lumen-bad-text`, `--lumen-focus`, `--lumen-good`, `--lumen-good-text`, `--lumen-hairline`, `--lumen-ink`, `--lumen-muted-ink`, `--lumen-series-1`, `--lumen-series-2`, `--lumen-series-3`, `--lumen-series-4`, `--lumen-series-5`, `--lumen-series-6`, `--lumen-warn`, `--lumen-warn-text`); this item's CSS also reads `--lumen-font-mono`, `--lumen-font-sans`, `--lumen-radius-k`. Never add Tailwind colour classes inside the component.

```tsx
import { ChartContainer, ChartTooltip, ChartTooltipContent, type ChartConfig } from "@/components/ui/lumen/charts/chart-shadcn";
```

## Parts

| Part | data-slot | What it is for |
|---|---|---|
| `ChartContainer` | — | Config, colours and the responsive Recharts wrapper. |
| `ChartTooltipContent` | — | shadcn's tooltip content, drawn in Lumen. |
| `ChartLegendContent` | — | shadcn's legend content, drawn in Lumen. |

## Sound
- Keep every `data-slot` and `data-sound` attribute: the sound layer reads them.
- Installing this item adds no audio. Nothing plays until the app mounts `GlassSoundProvider` once (install: `npx shadcn@latest add https://beautiful-ui.dev/r/glass-sound.json`, import from `@/components/beautiful-ui/glass-sound`); `GlassSoundToggle` is its mute control. Without a provider the audio engine never loads.

## Match the original
- Read `components/ui/lumen/charts/chart-shadcn.tsx` as the reference implementation before changing or recreating anything, and match it: sizes, colours per theme, motion timings, copy and behaviour.
- If you deviate (a prop you can't honour, a style you changed, a dependency you swapped), say so in your reply, part by part.
- Keep the accessibility contract, the keyboard map and the motion levels listed below.

## Use it when
- shadcn chart, ChartContainer, ChartTooltipContent, ChartLegendContent, copy a shadcn chart example, Recharts, migrate from shadcn charts, Lumen
- Code already written against shadcn's chart component
- Any Recharts chart you compose yourself, drawn in Lumen

### Not when
- New charts: Lumen Line, Area, Bar and the story charts do more (keyboard, tables, summaries) with less code

## Mistakes
- Import from the Lumen path, not @/components/ui/chart, or you get shadcn's look
- Give the container a height (min-h-[200px] or an aspect class): Recharts needs one

## Usage

```tsx
// Before (shadcn):
// import { ChartContainer, ChartTooltip, ChartTooltipContent, type ChartConfig } from "@/components/ui/chart";
import { ChartContainer, ChartTooltip, ChartTooltipContent, type ChartConfig } from "@/components/ui/lumen/charts/chart-shadcn";
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts";

const chartConfig = {
  desktop: { label: "Desktop", color: "var(--chart-1)" },
  mobile: { label: "Mobile", color: "var(--chart-2)" },
} satisfies ChartConfig;

<ChartContainer config={chartConfig} className="min-h-[200px] w-full">
  <BarChart accessibilityLayer data={chartData}>
    <CartesianGrid vertical={false} />
    <XAxis dataKey="month" tickLine={false} axisLine={false} />
    <ChartTooltip content={<ChartTooltipContent />} />
    <Bar dataKey="desktop" fill="var(--color-desktop)" radius={4} />
    <Bar dataKey="mobile" fill="var(--color-mobile)" radius={4} />
  </BarChart>
</ChartContainer>
```

## Props

| Prop | Type | Default | What it does |
|---|---|---|---|
| `ChartContainer config / id / className / children` | `ChartConfig / string / string / Recharts chart` |  | As shadcn's: sets --color-<key> for each series (light and dark) and makes the Recharts chart responsive. Default aspect 16:9; set a height or aspect class to change it. |
| `ChartConfig` | `{ [key]: { label?: ReactNode; icon?: ComponentType } & ({ color } \| { theme: { light, dark } }) }` |  | shadcn's type, exactly. |
| `ChartTooltipContent` | `indicator: dot \| line \| dashed; hideLabel; hideIndicator; labelFormatter; formatter; nameKey; labelKey; labelClassName; color` |  | As shadcn's, for <ChartTooltip content={<ChartTooltipContent />} />. |
| `ChartLegendContent` | `hideIcon; nameKey; verticalAlign` |  | As shadcn's, for <ChartLegend content={<ChartLegendContent />} />. |
| `useChart` | `() => { config }` |  | The container's config, for your own tooltip or legend. |

Full docs: https://beautiful-ui.dev/components/lumen-chart-shadcn

## Customising
- Colours: set the `--lumen-*` tokens on `:root`, or on a container with the `lumen-scope` class to retheme one area. Add the `lumen-inherit` class to follow your shadcn palette instead (`--chart-N`, `--destructive`).
- Dark mode follows the `.dark` class on an ancestor (the shadcn and next-themes convention).
- Update later by re-running the install with `--overwrite` (review the diff if you edited it). Changelog: https://beautiful-ui.dev/r/changelog.json

## Keyboard

| Keys | Action |
|---|---|
| ← / → | Move between points (Recharts' accessibility layer) |

## Motion inventory

| Interaction | What moves |
|---|---|
| Hover | Recharts' own animations; the tooltip and legend are Lumen's |

## Accessibility contract (preserve when editing)
- Recharts' accessibilityLayer (on by default in Recharts 3) gives keyboard access to the chart
- For a data table and a spoken summary, use Lumen's own charts or add Lumen Chart's ChartTable

## Install

Base UI project (a base-* style in components.json):

```bash
npx shadcn@latest add https://beautiful-ui.dev/r/lumen-chart-shadcn.json
```

Radix project (a radix-*, new-york or default style):

```bash
npx shadcn@latest add https://beautiful-ui.dev/r/radix-nova/lumen-chart-shadcn.json
```

Or add the `@beautiful-ui` registry to components.json and run `npx shadcn@latest add @beautiful-ui/lumen-chart-shadcn`: the CLI picks the build from your style.

## Credits
- shadcn/ui Chart (https://ui.shadcn.com/docs/components/chart)

## Source (Base UI build)

### components/ui/lumen/charts/chart-shadcn.tsx

```tsx
"use client";
// Generated from src/registry/core/charts/chart-shadcn.tsx by scripts/gen-systems.ts. Edit the core file, not this one.

import * as React from "react";
import * as RechartsPrimitive from "recharts";
import { cn } from "@/lib/utils";
import { ChartStyle as SystemChartStyle, type ChartConfig as SystemChartConfig } from "@/components/ui/lumen/chart";

/*
 * shadcn's chart API, exactly, drawn in the system: ChartContainer, ChartStyle, ChartTooltip,
 * ChartTooltipContent, ChartLegend, ChartLegendContent and useChart, with shadcn's ChartConfig
 * (labels may be any React node, and a series may carry an icon). Every chart example on
 * ui.shadcn.com/charts works by changing one import path: components/ui/chart becomes
 * components/ui/lumen/charts/chart-shadcn.
 *
 * Adapted from shadcn/ui's chart.tsx (MIT). The system's own charts (Line, Bar, the story charts)
 * keep their richer API; this is the drop-in for code written against shadcn's.
 */

export type ChartConfig = {
  [k in string]: {
    label?: React.ReactNode;
    icon?: React.ComponentType | undefined;
  } & ({ color?: string | undefined; theme?: never | undefined } | { color?: never | undefined; theme: Record<"light" | "dark", string> });
};

type ChartContextProps = { config: ChartConfig };

const ChartContext = React.createContext<ChartContextProps | null>(null);

function useChart() {
  const context = React.useContext(ChartContext);
  if (!context) throw new Error("useChart must be used within a <ChartContainer />");
  return context;
}

function ChartContainer({
  id,
  className,
  children,
  config,
  ...props
}: React.ComponentProps<"div"> & {
  config: ChartConfig;
  children: React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>["children"];
}) {
  const uniqueId = React.useId();
  const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
  return (
    <ChartContext.Provider value={{ config }}>
      <div data-slot="chart" data-chart={chartId} className={cn(`lumen-chart-shadcn`, className)} {...props}>
        <ChartStyle id={chartId} config={config} />
        <RechartsPrimitive.ResponsiveContainer>{children}</RechartsPrimitive.ResponsiveContainer>
      </div>
    </ChartContext.Provider>
  );
}

/** The --color-<key> variables for a config, light and dark (the system's escaping, shadcn's contract). */
function ChartStyle({ id, config }: { id: string; config: ChartConfig }) {
  // Only color and theme are read; labels and icons stay out of the stylesheet.
  const colors = Object.fromEntries(Object.entries(config).map(([k, c]) => [k, c.theme ? { theme: c.theme } : { color: c.color }])) as SystemChartConfig;
  return <SystemChartStyle id={id} config={colors} />;
}

const ChartTooltip = RechartsPrimitive.Tooltip;

type TooltipPayloadItem = {
  name?: string | number | undefined;
  value?: number | string | Array<number | string> | undefined;
  dataKey?: string | number | ((obj: unknown) => unknown) | undefined;
  color?: string | undefined;
  fill?: string | undefined;
  payload?: Record<string, unknown> & { fill?: string | undefined } | undefined;
  type?: string | undefined;
};

function ChartTooltipContent({
  active,
  payload,
  className,
  indicator = "dot",
  hideLabel = false,
  hideIndicator = false,
  label,
  labelFormatter,
  labelClassName,
  formatter,
  color,
  nameKey,
  labelKey,
}: Omit<React.ComponentProps<"div">, "color"> & {
  active?: boolean | undefined;
  payload?: readonly TooltipPayloadItem[] | undefined;
  label?: React.ReactNode;
  labelFormatter?: ((label: React.ReactNode, payload: readonly TooltipPayloadItem[]) => React.ReactNode) | undefined;
  formatter?: ((value: TooltipPayloadItem["value"], name: TooltipPayloadItem["name"], item: TooltipPayloadItem, index: number, payload: TooltipPayloadItem["payload"]) => React.ReactNode) | undefined;
  hideLabel?: boolean | undefined;
  hideIndicator?: boolean | undefined;
  indicator?: "line" | "dot" | "dashed" | undefined;
  nameKey?: string | undefined;
  labelKey?: string | undefined;
  labelClassName?: string | undefined;
  color?: string | undefined;
}) {
  const { config } = useChart();
  const tooltipLabel = React.useMemo(() => {
    if (hideLabel || !payload?.length) return null;
    const [item] = payload;
    const key = `${labelKey || item?.dataKey || item?.name || "value"}`;
    const itemConfig = getPayloadConfigFromPayload(config, item, key);
    const value = !labelKey && typeof label === "string" ? (config[label]?.label ?? label) : itemConfig?.label;
    if (labelFormatter) return <div className={cn(`lumen-chart-shadcn-tooltip-label`, labelClassName)}>{labelFormatter(value, payload)}</div>;
    if (!value) return null;
    return <div className={cn(`lumen-chart-shadcn-tooltip-label`, labelClassName)}>{value}</div>;
  }, [label, labelFormatter, payload, hideLabel, labelClassName, config, labelKey]);

  if (!active || !payload?.length) return null;
  const nestLabel = payload.length === 1 && indicator !== "dot";

  return (
    <div data-slot="chart-tooltip-content" className={cn(`lumen-chart-shadcn-tooltip`, className)}>
      {!nestLabel ? tooltipLabel : null}
      <div className={`lumen-chart-shadcn-tooltip-items`}>
        {payload
          .filter((item) => item.type !== "none")
          .map((item, index) => {
            const key = `${nameKey || item.name || item.dataKey || "value"}`;
            const itemConfig = getPayloadConfigFromPayload(config, item, key);
            const indicatorColor = color || item.payload?.fill || item.color;
            return (
              <div key={`${String(item.dataKey)}-${index}`} className={`lumen-chart-shadcn-tooltip-item`} data-indicator={indicator}>
                {formatter && item?.value !== undefined && item.name ? (
                  formatter(item.value, item.name, item, index, item.payload)
                ) : (
                  <>
                    {itemConfig?.icon ? (
                      <itemConfig.icon />
                    ) : (
                      !hideIndicator && <span aria-hidden className={`lumen-chart-shadcn-indicator`} data-indicator={indicator} style={{ "--color-bg": indicatorColor, "--color-border": indicatorColor } as React.CSSProperties} />
                    )}
                    <div className={`lumen-chart-shadcn-tooltip-row`} data-nested={nestLabel || undefined}>
                      <div className={`lumen-chart-shadcn-tooltip-name`}>
                        {nestLabel ? tooltipLabel : null}
                        <span>{itemConfig?.label || item.name}</span>
                      </div>
                      {item.value != null && <span className={`lumen-chart-shadcn-tooltip-value`}>{typeof item.value === "number" ? item.value.toLocaleString() : String(item.value)}</span>}
                    </div>
                  </>
                )}
              </div>
            );
          })}
      </div>
    </div>
  );
}

const ChartLegend = RechartsPrimitive.Legend;

function ChartLegendContent({
  className,
  hideIcon = false,
  payload,
  verticalAlign = "bottom",
  nameKey,
}: React.ComponentProps<"div"> & {
  payload?: readonly { value?: string | undefined; dataKey?: string | number | ((obj: unknown) => unknown) | undefined; color?: string | undefined; type?: string | undefined }[] | undefined;
  verticalAlign?: "top" | "middle" | "bottom" | undefined;
  hideIcon?: boolean | undefined;
  nameKey?: string | undefined;
}) {
  const { config } = useChart();
  if (!payload?.length) return null;
  return (
    <div data-slot="chart-legend-content" data-align={verticalAlign} className={cn(`lumen-chart-shadcn-legend`, className)}>
      {payload
        .filter((item) => item.type !== "none")
        .map((item) => {
          const key = `${nameKey || item.dataKey || "value"}`;
          const itemConfig = getPayloadConfigFromPayload(config, item, key);
          return (
            <div key={String(item.value)} className={`lumen-chart-shadcn-legend-item`}>
              {itemConfig?.icon && !hideIcon ? <itemConfig.icon /> : <span aria-hidden className={`lumen-chart-shadcn-legend-swatch`} style={{ backgroundColor: item.color }} />}
              {itemConfig?.label}
            </div>
          );
        })}
    </div>
  );
}

/** The config entry for a tooltip or legend item, by key, as shadcn resolves it. */
function getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: string) {
  if (typeof payload !== "object" || payload === null) return undefined;
  const inner = "payload" in payload && typeof payload.payload === "object" && payload.payload !== null ? (payload.payload as Record<string, unknown>) : undefined;
  let configLabelKey: string = key;
  if (key in payload && typeof (payload as Record<string, unknown>)[key] === "string") configLabelKey = (payload as Record<string, unknown>)[key] as string;
  else if (inner && key in inner && typeof inner[key] === "string") configLabelKey = inner[key] as string;
  return configLabelKey in config ? config[configLabelKey] : config[key];
}

export { ChartContainer, ChartTooltip, ChartTooltipContent, ChartLegend, ChartLegendContent, ChartStyle, useChart };
```

### CSS (the registry `css` / `cssVars`, merged into the global stylesheet by the shadcn CLI)

```css
@layer components {
  .lumen-chart-shadcn {
    display: flex;
    justify-content: center;
    aspect-ratio: 16/9;
    min-width: 0;
    color: var(--gc-ink);
    font: 400 12px/1.4 var(--lumen-font-sans, var(--font-sans, var(--font-geist, var(--font-geist-sans, 'Geist')))), system-ui, sans-serif;
  }
  .lumen-chart-shadcn .recharts-cartesian-axis-tick text {
    fill: var(--gc-sec);
    font: 500 10.5px/1 var(--lumen-font-mono, var(--font-mono, var(--font-geist-mono, 'Geist Mono'))), ui-monospace, monospace;
  }
  .lumen-chart-shadcn .recharts-cartesian-grid line[stroke="#ccc"],.lumen-chart-shadcn .recharts-cartesian-grid line {
    stroke: var(--gc-hair);
  }
  .lumen-chart-shadcn .recharts-curve.recharts-tooltip-cursor {
    stroke: var(--gc-hair);
  }
  .lumen-chart-shadcn .recharts-rectangle.recharts-tooltip-cursor {
    fill: color-mix(in srgb,var(--gc-ink) 6%,transparent);
  }
  .lumen-chart-shadcn .recharts-polar-grid [stroke="#ccc"] {
    stroke: var(--gc-hair);
  }
  .lumen-chart-shadcn .recharts-reference-line [stroke="#ccc"] {
    stroke: var(--gc-hair);
  }
  .lumen-chart-shadcn :is(.recharts-dot[stroke="#fff"],.recharts-sector[stroke="#fff"]) {
    stroke: transparent;
  }
  .lumen-chart-shadcn :is(.recharts-layer,.recharts-surface,.recharts-sector) {
    outline: none;
  }
  .lumen-chart-shadcn-tooltip {
    display: grid;
    gap: 6px;
    min-width: 8rem;
    padding: 9px 11px;
    border-radius: calc(12px * var(--lumen-radius-k, 1));
    background: var(--gc-tipBg,rgba(12,12,16,.92));
    box-shadow: inset 0 0 0 1px var(--gc-hair),0 12px 32px -12px rgba(0,0,0,.5);
    backdrop-filter: blur(12px);
    color: var(--gc-ink);
    font: 400 12px/1.35 var(--lumen-font-sans, var(--font-sans, var(--font-geist, var(--font-geist-sans, 'Geist')))), system-ui, sans-serif;
  }
  .lumen-chart-shadcn-tooltip-label {
    font: 500 10.5px/1.3 var(--lumen-font-mono, var(--font-mono, var(--font-geist-mono, 'Geist Mono'))), ui-monospace, monospace;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--gc-sec);
  }
  .lumen-chart-shadcn-tooltip-items {
    display: grid;
    gap: 5px;
  }
  .lumen-chart-shadcn-tooltip-item {
    display: flex;
    align-items: stretch;
    gap: 8px;
  }
  .lumen-chart-shadcn-tooltip-item[data-indicator="dot"] {
    align-items: center;
  }
  .lumen-chart-shadcn-tooltip-item>svg {
    width: 12px;
    height: 12px;
    color: var(--gc-sec);
  }
  .lumen-chart-shadcn-indicator {
    flex: none;
    border-radius: calc(2px * var(--lumen-radius-k, 1));
    background: var(--color-bg);
    border-color: var(--color-border);
  }
  .lumen-chart-shadcn-indicator[data-indicator="dot"] {
    width: 8px;
    height: 8px;
    box-shadow: 0 0 6px color-mix(in srgb,var(--color-bg) 60%,transparent);
  }
  .lumen-chart-shadcn-indicator[data-indicator="line"] {
    width: 3px;
  }
  .lumen-chart-shadcn-indicator[data-indicator="dashed"] {
    width: 0;
    border: 1.5px dashed var(--color-border);
    background: transparent;
  }
  .lumen-chart-shadcn-tooltip-row {
    display: flex;
    flex: 1;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    line-height: 1;
  }
  .lumen-chart-shadcn-tooltip-row[data-nested] {
    align-items: flex-end;
  }
  .lumen-chart-shadcn-tooltip-name {
    display: grid;
    gap: 5px;
    color: var(--gc-sec);
  }
  .lumen-chart-shadcn-tooltip-value {
    font: 500 12px/1 var(--lumen-font-mono, var(--font-mono, var(--font-geist-mono, 'Geist Mono'))), ui-monospace, monospace;
    font-variant-numeric: tabular-nums;
    color: var(--gc-ink);
  }
  .lumen-chart-shadcn-legend {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 14px;
    padding-top: 12px;
  }
  .lumen-chart-shadcn-legend[data-align="top"] {
    padding-top: 0;
    padding-bottom: 12px;
  }
  .lumen-chart-shadcn-legend-item {
    display: flex;
    align-items: center;
    gap: 6px;
    color: var(--gc-sec);
  }
  .lumen-chart-shadcn-legend-item>svg {
    width: 12px;
    height: 12px;
  }
  .lumen-chart-shadcn-legend-swatch {
    width: 8px;
    height: 8px;
    border-radius: calc(2px * var(--lumen-radius-k, 1));
  }
}
```

The lumen foundation (the tokens listed above, keyframes and motion levels) installs once with the first component; its CSS is public at https://beautiful-ui.dev/r/lumen-foundation.json.
