{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "chart",
  "title": "chart",
  "description": "shadcn's Recharts 3 chart wrapper: ChartContainer, ChartTooltip, ChartTooltipContent, ChartLegend, ChartLegendContent and ChartStyle. A ChartConfig colour such as \"chart-1\" picks one of the theme's five series colours; axis text, grid, cursor and tooltip take theme tokens. The series helpers assign distinct colours per set of series, keep a missing bucket a gap unless the caller passes missing=\"zero\" for a measured range, step date ticks so labels never collide, and carry the quiet time-series preset (horizontal guides, no per-bucket dots).",
  "dependencies": [
    "recharts@^3",
    "class-variance-authority@^0.7"
  ],
  "registryDependencies": [
    "@dwellir/dwellir-theme",
    "@dwellir/utils"
  ],
  "files": [
    {
      "path": "components/ui/chart.tsx",
      "content": "'use client';\n\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport {\n  type ComponentProps,\n  type ComponentType,\n  cloneElement,\n  createContext,\n  isValidElement,\n  type ReactNode,\n  useContext,\n  useId,\n} from 'react';\nimport type {\n  DefaultLegendContentProps,\n  DefaultTooltipContentProps,\n  TooltipValueType,\n} from 'recharts';\nimport * as RechartsPrimitive from 'recharts';\nimport { type ChartMissing, fillMissingSeriesValues } from '@/components/ui/chart-series';\nimport { cn } from '@/lib/utils';\n\n/* The theme's five series colours. ui-chart maps them onto --chart-series-1 to 5. */\ntype ChartColor = 'chart-1' | 'chart-2' | 'chart-3' | 'chart-4' | 'chart-5';\n\n/* A series colour: a theme series, or a token as upstream writes it, such as var(--chart-1). */\ntype ChartToken = ChartColor | `var(--${string})`;\n\n/*\n * One entry per series, keyed by its dataKey. The chart sets --color-<key> for\n * each, so a series reads fill=\"var(--color-<key>)\". The theme series, chart-1\n * to chart-5, already differ in light and dark. As upstream, `theme` sets one\n * token per mode instead; neither takes a raw colour.\n */\nexport type ChartConfig = Record<\n  string,\n  { label?: ReactNode; icon?: ComponentType } & (\n    | { color?: ChartToken; theme?: never }\n    | { color?: never; theme: Record<keyof typeof THEMES, ChartToken> }\n  )\n>;\n\n/* The dark mode selector: .dark in a registry app, .next-dark-theme on the Dwellir site. */\nconst THEMES = { light: '', dark: ':is(.dark, .next-dark-theme) ' } as const;\n\nconst ChartContext = createContext<{ config: ChartConfig } | null>(null);\n\nfunction useChart() {\n  const context = useContext(ChartContext);\n  if (!context) throw new Error('useChart must be used within a <ChartContainer />');\n  return context;\n}\n\nconst INITIAL_DIMENSION = { width: 320, height: 200 } as const;\n\ntype ChartLabelling = Pick<\n  ComponentProps<'svg'>,\n  'aria-label' | 'aria-labelledby' | 'aria-describedby'\n>;\n\n/* The chart's own SVG takes the name: a Recharts chart passes these attributes on to it. */\nfunction nameChart(children: ReactNode, labelling: ChartLabelling) {\n  const set = Object.fromEntries(Object.entries(labelling).filter(([, value]) => value));\n  return isValidElement(children) ? cloneElement(children, set) : children;\n}\n\n/*\n * For a measured range (`missing=\"zero\"`), the child chart's rows with every\n * missing series value at 0, keyed by the config's series. In the default\n * `gap` mode the rows pass through untouched: a missing value stays missing\n * and the series breaks there, with `connectNulls` off, instead of bridging\n * across days nobody measured. A child whose data is not a list of rows\n * (such as a pie chart's) also passes through untouched.\n */\nfunction withFilledData(children: ReactNode, config: ChartConfig, missing: ChartMissing) {\n  if (missing !== 'zero' || !isValidElement<{ data?: unknown }>(children)) return children;\n  const { data } = children.props;\n  const rows = Array.isArray(data) ? data : [];\n  if (!rows.length || rows.some((row) => typeof row !== 'object' || row === null)) return children;\n  const filled = fillMissingSeriesValues(\n    rows as Record<string, unknown>[],\n    Object.keys(config),\n    'zero',\n  );\n  return cloneElement(children, { data: filled });\n}\n\n/*\n * The frame of a Recharts chart: it sizes the chart to its box (16:9 unless the\n * caller sets a height). aria-label or aria-labelledby names the chart, such as\n * by its card title; `description` is its text alternative, read as the\n * chart's description and hidden on screen. `missing` says what a bucket\n * with no value for a series means: `gap` (the default) leaves it missing, so\n * unknown data breaks the line; `zero` fills it with 0, for a range the\n * caller measured. Pass `zero` only then.\n */\nfunction ChartContainer({\n  id,\n  className,\n  children,\n  config,\n  missing = 'gap',\n  initialDimension = INITIAL_DIMENSION,\n  description,\n  'aria-label': ariaLabel,\n  'aria-labelledby': ariaLabelledBy,\n  'aria-describedby': ariaDescribedBy,\n  ...props\n}: ComponentProps<'div'> & {\n  config: ChartConfig;\n  missing?: ChartMissing;\n  children: ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>['children'];\n  initialDimension?: { width: number; height: number };\n  description?: ReactNode;\n}) {\n  const uniqueId = useId();\n  const chartId = `chart-${(id ?? uniqueId).replace(/[^\\w-]/g, '')}`;\n  const descriptionId = description ? `${chartId}-description` : undefined;\n  const describedBy = [ariaDescribedBy, descriptionId].filter(Boolean).join(' ') || undefined;\n  return (\n    <ChartContext.Provider value={{ config }}>\n      <div\n        data-slot=\"chart\"\n        data-chart={chartId}\n        className={cn('ui-chart flex aspect-video justify-center text-xs', className)}\n        {...props}\n      >\n        <ChartStyle id={chartId} config={config} />\n        {description ? (\n          <div data-slot=\"chart-description\" id={descriptionId} className=\"sr-only\">\n            {description}\n          </div>\n        ) : null}\n        <RechartsPrimitive.ResponsiveContainer initialDimension={initialDimension}>\n          {nameChart(withFilledData(children, config, missing), {\n            'aria-label': ariaLabel,\n            'aria-labelledby': ariaLabelledBy,\n            'aria-describedby': describedBy,\n          })}\n        </RechartsPrimitive.ResponsiveContainer>\n      </div>\n    </ChartContext.Provider>\n  );\n}\n\n/* A config token as CSS: a theme series reads its --chart-series-N, another token stays as it is. */\nfunction seriesValue(token: string | undefined) {\n  const series = token?.match(/^(?:chart-([1-5])|var\\(--chart-([1-5])\\))$/);\n  if (series) return `var(--chart-series-${series[1] ?? series[2]})`;\n  return token && /^var\\(--[\\w-]+\\)$/.test(token) ? token : undefined;\n}\n\n/* The --color-<key> declarations of one mode. Dark sets only the series with a `theme` pair. */\nfunction seriesLines(config: ChartConfig, mode: keyof typeof THEMES) {\n  return Object.entries(config).flatMap(([key, item]) => {\n    const value = seriesValue(item.theme?.[mode] ?? (mode === 'light' ? item.color : undefined));\n    return value && /^[\\w-]+$/.test(key) ? [`  --color-${key}: ${value};`] : [];\n  });\n}\n\n/*\n * Writes --color-<key> for each series of one chart. The config's type and\n * seriesValue limit every value to a token, so the rule carries no colour of its own.\n */\nfunction ChartStyle({ id, config }: { id: string; config: ChartConfig }) {\n  const css = Object.entries(THEMES)\n    .map(([mode, prefix]) => {\n      const lines = seriesLines(config, mode as keyof typeof THEMES);\n      return lines.length ? `${prefix}[data-chart=\"${id}\"] {\\n${lines.join('\\n')}\\n}` : '';\n    })\n    .filter(Boolean)\n    .join('\\n');\n  if (!css) return null;\n  // oxlint-disable-next-line shadcn/no-inline-styles -- per-chart series keys need a rule; it maps them onto theme tokens only.\n  return <style data-slot=\"chart-style\">{css}</style>;\n}\n\n/*\n * The swatch before a series name. An SVG paints it, so the series colour\n * (often a CSS variable) needs no inline style. The SVG fills the swatch out\n * of flow: in flow, a line or dashed swatch would take the SVG's 150 px\n * default height and stretch its row to it.\n */\nconst chartIndicatorVariants = cva('relative flex shrink-0', {\n  variants: {\n    indicator: {\n      dot: 'size-2.5',\n      line: 'w-1 self-stretch',\n      dashed: 'w-0.5 self-stretch',\n    },\n  },\n  defaultVariants: { indicator: 'dot' },\n});\n\nfunction ChartIndicator({\n  color,\n  indicator,\n  className,\n}: { color?: string; className?: string } & VariantProps<typeof chartIndicatorVariants>) {\n  return (\n    <span\n      data-slot=\"chart-indicator\"\n      aria-hidden=\"true\"\n      className={cn(chartIndicatorVariants({ indicator }), className)}\n    >\n      <svg\n        data-slot=\"chart-indicator-mark\"\n        className=\"absolute inset-0 size-full\"\n        aria-hidden=\"true\"\n      >\n        {indicator === 'dashed' ? (\n          <line\n            data-slot=\"chart-indicator-line\"\n            x1=\"50%\"\n            x2=\"50%\"\n            y1=\"0\"\n            y2=\"100%\"\n            stroke={color}\n            strokeWidth={1.5}\n            strokeDasharray=\"3 2\"\n          />\n        ) : (\n          <rect data-slot=\"chart-indicator-fill\" width=\"100%\" height=\"100%\" rx={2} fill={color} />\n        )}\n      </svg>\n    </span>\n  );\n}\n\n/* The config entry for a payload item: by the value of `key` in the item, else by `key` itself. */\nfunction getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: string) {\n  if (typeof payload !== 'object' || payload === null) return undefined;\n  const inner =\n    'payload' in payload && typeof payload.payload === 'object' && payload.payload !== null\n      ? (payload.payload as Record<string, unknown>)\n      : undefined;\n  const own = (payload as Record<string, unknown>)[key];\n  const nested = inner?.[key];\n  const labelKey = typeof own === 'string' ? own : typeof nested === 'string' ? nested : key;\n  return labelKey in config ? config[labelKey] : config[key];\n}\n\nconst ChartTooltip = RechartsPrimitive.Tooltip;\n\ntype TooltipItem = NonNullable<\n  DefaultTooltipContentProps<TooltipValueType, number | string>['payload']\n>[number];\n\ntype ChartTooltipContentProps = ComponentProps<typeof RechartsPrimitive.Tooltip> &\n  ComponentProps<'div'> & {\n    hideLabel?: boolean;\n    hideIndicator?: boolean;\n    indicator?: NonNullable<VariantProps<typeof chartIndicatorVariants>['indicator']>;\n    nameKey?: string;\n    labelKey?: string;\n  } & Omit<DefaultTooltipContentProps<TooltipValueType, number | string>, 'accessibilityLayer'>;\n\nfunction TooltipLabel({\n  payload,\n  label,\n  labelKey,\n  labelFormatter,\n  labelClassName,\n}: Pick<\n  ChartTooltipContentProps,\n  'payload' | 'label' | 'labelKey' | 'labelFormatter' | 'labelClassName'\n>) {\n  const { config } = useChart();\n  if (!payload?.length) return null;\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' ? (config[label]?.label ?? label) : itemConfig?.label;\n  if (!labelFormatter && !value) return null;\n  return (\n    <div\n      data-slot=\"chart-tooltip-label\"\n      className={cn('font-bold text-foreground', labelClassName)}\n    >\n      {labelFormatter ? labelFormatter(value, payload) : value}\n    </div>\n  );\n}\n\ntype TooltipRowProps = Pick<\n  ChartTooltipContentProps,\n  'hideIndicator' | 'nameKey' | 'color' | 'formatter'\n> & {\n  item: TooltipItem;\n  index: number;\n  indicator: NonNullable<ChartTooltipContentProps['indicator']>;\n  nestLabel: boolean;\n  tooltipLabel: ReactNode;\n};\n\nfunction formatValue(value: TooltipItem['value']) {\n  return typeof value === 'number' ? value.toLocaleString() : String(value);\n}\n\n/* A series in the tooltip: its icon or swatch, its name and its value. */\nfunction TooltipSeries({\n  item,\n  indicator,\n  hideIndicator,\n  nestLabel,\n  tooltipLabel,\n  nameKey,\n  color,\n}: Omit<TooltipRowProps, 'index' | 'formatter'>) {\n  const { config } = useChart();\n  const key = `${nameKey ?? item.name ?? item.dataKey ?? 'value'}`;\n  const itemConfig = getPayloadConfigFromPayload(config, item, key);\n  const Icon = itemConfig?.icon;\n  const swatch = hideIndicator ? null : (\n    <ChartIndicator\n      color={color ?? item.payload?.fill ?? item.color}\n      indicator={indicator}\n      className={nestLabel && indicator === 'dashed' ? 'my-0.5' : undefined}\n    />\n  );\n  return (\n    <>\n      {Icon ? <Icon /> : swatch}\n      <div\n        data-slot=\"chart-tooltip-series\"\n        className={cn(\n          'flex flex-1 justify-between gap-4 leading-none',\n          nestLabel ? 'items-end' : 'items-center',\n        )}\n      >\n        <div data-slot=\"chart-tooltip-name\" className=\"grid gap-1.5\">\n          {nestLabel ? tooltipLabel : null}\n          <span data-slot=\"chart-tooltip-series-label\" className=\"text-muted-foreground\">\n            {itemConfig?.label ?? item.name}\n          </span>\n        </div>\n        {item.value != null ? (\n          <span data-slot=\"chart-tooltip-value\" className=\"font-mono text-foreground tabular-nums\">\n            {formatValue(item.value)}\n          </span>\n        ) : null}\n      </div>\n    </>\n  );\n}\n\nfunction TooltipRow({ index, formatter, ...series }: TooltipRowProps) {\n  const { item, indicator } = series;\n  const custom = formatter && item.value !== undefined && item.name;\n  return (\n    <div\n      data-slot=\"chart-tooltip-item\"\n      className={cn(\n        'flex w-full flex-wrap items-stretch gap-2 [&>svg]:size-2.5 [&>svg]:text-muted-foreground',\n        indicator === 'dot' && 'items-center',\n      )}\n    >\n      {custom ? (\n        formatter(item.value, item.name, item, index, item.payload)\n      ) : (\n        <TooltipSeries {...series} />\n      )}\n    </div>\n  );\n}\n\n/* The tooltip panel: the popover surface, one row per series with its swatch and value. */\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}: ChartTooltipContentProps) {\n  if (!active || !payload?.length) return null;\n  const nestLabel = payload.length === 1 && indicator !== 'dot';\n  const tooltipLabel = hideLabel ? null : (\n    <TooltipLabel\n      payload={payload}\n      label={label}\n      labelKey={labelKey}\n      labelFormatter={labelFormatter}\n      labelClassName={labelClassName}\n    />\n  );\n  return (\n    <div\n      data-slot=\"chart-tooltip-content\"\n      className={cn(\n        'grid min-w-32 items-start gap-1.5 rounded-lg border border-border bg-popover px-2.5 py-1.5 text-xs text-popover-foreground shadow-popover',\n        className,\n      )}\n    >\n      {nestLabel ? null : tooltipLabel}\n      <div data-slot=\"chart-tooltip-items\" className=\"grid gap-1.5\">\n        {payload\n          .filter((item) => item.type !== 'none')\n          .map((item, index) => (\n            <TooltipRow\n              key={`${item.dataKey ?? item.name ?? index}`}\n              item={item}\n              index={index}\n              indicator={indicator}\n              hideIndicator={hideIndicator}\n              nestLabel={nestLabel}\n              tooltipLabel={tooltipLabel}\n              nameKey={nameKey}\n              color={color}\n              formatter={formatter}\n            />\n          ))}\n      </div>\n    </div>\n  );\n}\n\nconst ChartLegend = RechartsPrimitive.Legend;\n\n/* The legend: each series' swatch and label, in a centred row. */\nfunction ChartLegendContent({\n  className,\n  hideIcon = false,\n  payload,\n  verticalAlign = 'bottom',\n  nameKey,\n}: ComponentProps<'div'> & {\n  hideIcon?: boolean;\n  nameKey?: string;\n} & DefaultLegendContentProps) {\n  const { config } = useChart();\n  if (!payload?.length) return null;\n  return (\n    <div\n      data-slot=\"chart-legend-content\"\n      className={cn(\n        'flex flex-wrap 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) => {\n          const key = `${nameKey ?? item.dataKey ?? 'value'}`;\n          const itemConfig = getPayloadConfigFromPayload(config, item, key);\n          return (\n            <div\n              key={key}\n              data-slot=\"chart-legend-item\"\n              className=\"flex items-center gap-1.5 text-muted-foreground [&>svg]:size-3\"\n            >\n              {itemConfig?.icon && !hideIcon ? (\n                <itemConfig.icon />\n              ) : (\n                <ChartIndicator color={item.color} className=\"size-2\" />\n              )}\n              <span data-slot=\"chart-legend-label\" className=\"text-foreground\">\n                {itemConfig?.label}\n              </span>\n            </div>\n          );\n        })}\n    </div>\n  );\n}\n\nexport {\n  assignSeriesColors,\n  CHART_SERIES_COLORS,\n  type ChartMissing,\n  type ChartSeriesColor,\n  chartConfigForSeries,\n  chartDateAxisProps,\n  fillMissingSeriesValues,\n  QUIET_GRID_PROPS,\n  QUIET_SERIES_PROPS,\n  type SeriesChartConfig,\n  TIME_SERIES_MARGIN,\n  visibleDateTickIndexes,\n} from '@/components/ui/chart-series';\nexport {\n  ChartContainer,\n  ChartIndicator,\n  ChartLegend,\n  ChartLegendContent,\n  ChartStyle,\n  ChartTooltip,\n  ChartTooltipContent,\n  chartIndicatorVariants,\n};\n",
      "type": "registry:ui"
    },
    {
      "path": "components/ui/chart-series.ts",
      "content": "import {\n  type ComponentType,\n  createElement,\n  type ReactNode,\n  useEffect,\n  useRef,\n  useState,\n} from 'react';\nimport type { DotItemDotProps } from 'recharts';\n\n/*\n * Helpers for honest, readable time series. They carry no classes and no\n * colours of their own: a series colour is always one of the theme's five\n * chart tokens, which already differ in light and dark.\n */\n\n/* The theme's five series colours, in palette order. */\nexport const CHART_SERIES_COLORS = ['chart-1', 'chart-2', 'chart-3', 'chart-4', 'chart-5'] as const;\n\nexport type ChartSeriesColor = (typeof CHART_SERIES_COLORS)[number];\n\n/* A name's stable starting place in a palette of `size`. */\nfunction hashIndex(name: string, size: number) {\n  let hash = 1091;\n  for (let i = 0; i < name.length; i += 1) hash = (hash * 33) ^ name.charCodeAt(i);\n  return Math.abs(hash) % size;\n}\n\n/* The first free colour from a name's hashed colour on, or its hashed colour when none is free. */\nfunction freeColor(name: string, used: ReadonlySet<ChartSeriesColor>): ChartSeriesColor {\n  const start = hashIndex(name, CHART_SERIES_COLORS.length);\n  for (let step = 0; step < CHART_SERIES_COLORS.length; step += 1) {\n    const color = CHART_SERIES_COLORS[(start + step) % CHART_SERIES_COLORS.length];\n    if (!used.has(color)) return color;\n  }\n  return CHART_SERIES_COLORS[start];\n}\n\n/**\n * One theme colour per series name, distinct while the five-colour palette\n * lasts. Each name starts at its hashed colour and takes the next free one\n * on a clash. Names are placed in sorted order, so the result depends only\n * on the set of names, never on their order. `reserved` colours, taken by\n * series with a colour of their own, are left free of new assignments while\n * the palette lasts. Past five series a colour repeats; group the rest, as\n * an \"Others\" series, instead of asking readers to tell repeats apart.\n */\nexport function assignSeriesColors(\n  names: readonly string[],\n  reserved: readonly ChartSeriesColor[] = [],\n): Map<string, ChartSeriesColor> {\n  const used = new Set<ChartSeriesColor>(reserved);\n  const colors = new Map<string, ChartSeriesColor>();\n  for (const name of [...new Set(names)].sort()) {\n    const color = freeColor(name, used);\n    used.add(color);\n    colors.set(name, color);\n  }\n  return colors;\n}\n\nexport type SeriesChartConfig = Record<\n  string,\n  { label?: ReactNode; icon?: ComponentType; color: ChartSeriesColor }\n>;\n\ntype SeriesEntry =\n  | string\n  | { key: string; label?: ReactNode; icon?: ComponentType; color?: ChartSeriesColor };\n\nconst entryKey = (entry: SeriesEntry) => (typeof entry === 'string' ? entry : entry.key);\n\n/**\n * A `ChartConfig` for a set of series: each entry keeps its own label, icon\n * and colour, and an entry without a colour takes a distinct one from\n * `assignSeriesColors`. Pass series keys as plain strings when the key is\n * also the label.\n */\nexport function chartConfigForSeries(series: readonly SeriesEntry[]): SeriesChartConfig {\n  const reserved = series.flatMap((entry) =>\n    typeof entry !== 'string' && entry.color ? [entry.color] : [],\n  );\n  const assigned = assignSeriesColors(\n    series.filter((entry) => typeof entry === 'string' || !entry.color).map(entryKey),\n    reserved,\n  );\n  return Object.fromEntries(\n    series.map((entry) => {\n      const key = entryKey(entry);\n      const label = typeof entry === 'string' ? key : (entry.label ?? key);\n      const icon = typeof entry === 'string' ? undefined : entry.icon;\n      const own = typeof entry === 'string' ? undefined : entry.color;\n      return [key, { label, icon, color: own ?? assigned.get(key) ?? CHART_SERIES_COLORS[0] }];\n    }),\n  );\n}\n\n/**\n * What a missing bucket means. `gap`: no reading, so the line breaks and\n * says so. `zero`: the range was measured and the series was idle, so the\n * bucket reads 0. The default everywhere is `gap`; a caller passes `zero`\n * only for a measured range. On a status page a missing bucket drawn as\n * zero would show a false outage or a false calm.\n */\nexport type ChartMissing = 'zero' | 'gap';\n\nconst isMissing = (value: unknown) =>\n  value == null || (typeof value === 'number' && !Number.isFinite(value));\n\n/* Filled rows by their input rows: the same inputs must return the same rows. */\nconst filledCache = new WeakMap<readonly Record<string, unknown>[], Map<string, unknown[]>>();\n\n/**\n * The rows with every missing series value made explicit: `0` for a measured\n * range, `null` for unknown data. A `null` breaks a line or area series, so\n * nothing bridges across empty buckets. The input rows stay untouched and\n * are treated as immutable: the same data, series keys and mode return the\n * same rows, so a re-render never restarts a chart's data animation.\n */\nexport function fillMissingSeriesValues<T extends Record<string, unknown>>(\n  data: readonly T[],\n  seriesKeys: readonly string[],\n  missing: ChartMissing = 'gap',\n): T[] {\n  const cacheKey = `${missing}:${seriesKeys.join('\\0')}`;\n  const cached = filledCache.get(data)?.get(cacheKey);\n  if (cached) return cached as T[];\n  const replacement = missing === 'zero' ? 0 : null;\n  const filled = data.map((row) => {\n    const next: Record<string, unknown> = { ...row };\n    for (const key of seriesKeys) {\n      if (isMissing(next[key])) next[key] = replacement;\n    }\n    return next as T;\n  });\n  const byKey = filledCache.get(data) ?? new Map<string, unknown[]>();\n  byKey.set(cacheKey, filled);\n  filledCache.set(data, byKey);\n  return filled;\n}\n\n/* Labels need this much room each, in px, before the axis steps over one. */\nconst MIN_TICK_SPACING = 72;\n\n/* With no measured width, assume a phone plot: four labels still fit at 320 px. */\nconst FALLBACK_TICK_COUNT = 4;\n\n/**\n * The point indexes whose axis label shows. Pass the measured chart width\n * (`useChartWidth` measures it): the axis then shows as many labels as the\n * 72 px budget allows. With no width it assumes a narrow phone plot and\n * keeps at most four. The first and last label always show, and the rest\n * spread evenly between them, so no two labels sit a bucket apart at the\n * axis end.\n */\nexport function visibleDateTickIndexes(\n  pointCount: number,\n  chartWidth = 0,\n  minLabelSpacing = MIN_TICK_SPACING,\n): number[] {\n  const count = Math.max(0, Math.floor(pointCount));\n  if (count <= 1) return count === 1 ? [0] : [];\n  const target =\n    chartWidth > 0\n      ? Math.max(2, Math.floor(chartWidth / Math.max(1, minLabelSpacing)))\n      : Math.min(count, FALLBACK_TICK_COUNT);\n  if (count <= target) return Array.from({ length: count }, (_, index) => index);\n  const last = count - 1;\n  const visible = Array.from({ length: target }, (_, slot) =>\n    Math.round((slot * last) / (target - 1)),\n  );\n  return [...new Set(visible)];\n}\n\n/**\n * The width of the element that sizes a chart, measured live. Put the ref on\n * the `ChartContainer` (or the element that wraps it) and pass the width to\n * `chartDateAxisProps`, so the date axis steps to the real plot instead of\n * the phone fallback. The width reads 0 until the element mounts.\n */\nexport function useChartWidth<T extends HTMLElement = HTMLDivElement>() {\n  const ref = useRef<T | null>(null);\n  const [width, setWidth] = useState(0);\n  useEffect(() => {\n    const element = ref.current;\n    if (!element) return undefined;\n    const measure = () => setWidth(element.clientWidth);\n    measure();\n    if (typeof ResizeObserver === 'undefined') {\n      window.addEventListener('resize', measure);\n      return () => window.removeEventListener('resize', measure);\n    }\n    const observer = new ResizeObserver(measure);\n    observer.observe(element);\n    return () => observer.disconnect();\n  }, []);\n  return [ref, width] as const;\n}\n\n/**\n * XAxis props for a date axis that never collides: spread them after\n * `dataKey`, with the measured width from `useChartWidth`. `tickFormatter`\n * blanks the stepped-over labels, so pass your own formatter as `format` to\n * keep it for the labels that show.\n */\nexport function chartDateAxisProps(\n  pointCount: number,\n  chartWidth = 0,\n  format?: (value: string, index: number) => string,\n) {\n  const visible = new Set(visibleDateTickIndexes(pointCount, chartWidth));\n  return {\n    interval: 0 as const,\n    minTickGap: 0,\n    tickFormatter: (value: string, index: number) =>\n      visible.has(index) ? (format ? format(value, index) : value) : '',\n  };\n}\n\n/* A measurement with no neighbouring segment needs a marker to remain visible. */\nfunction isolatedPointDot({\n  cx,\n  cy,\n  points,\n  index,\n  stroke,\n  strokeOpacity,\n  opacity,\n}: DotItemDotProps) {\n  const hasPoint = (offset: number) => {\n    const point = points[index + offset];\n    return point != null && Number.isFinite(point.x) && Number.isFinite(point.y);\n  };\n  if (!hasPoint(0) || hasPoint(-1) || hasPoint(1)) return null;\n  return createElement('circle', {\n    'data-slot': 'chart-isolated-point',\n    cx,\n    cy,\n    r: 3,\n    fill: stroke,\n    fillOpacity: strokeOpacity,\n    opacity,\n  });\n}\n\n/**\n * The quiet preset keeps horizontal guides and marks isolated measurements.\n * Connected points show a dot only when active. Gaps stay disconnected.\n * The margin leaves room for the first and last axis labels.\n */\nexport const QUIET_GRID_PROPS = { vertical: false } as const;\nexport const QUIET_SERIES_PROPS = {\n  dot: isolatedPointDot,\n  connectNulls: false,\n  strokeWidth: 2,\n  activeDot: { r: 4, strokeWidth: 0 },\n} as const;\nexport const TIME_SERIES_MARGIN = { top: 8, right: 32, bottom: 0, left: 16 } as const;\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}