{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "chart-legend",
  "title": "chart-legend",
  "description": "A chart legend laid out beside the chart: series markers, muted labels, muted secondary detail, optional right-aligned values, and an icon slot. Rows align on phones and wrap on wide screens (layout=\"rows\" or \"wrap\" pins a shape); hover and keyboard focus highlight alike; long lists collapse after maxVisible rows into \"+N more\".",
  "dependencies": [
    "class-variance-authority@^0.7"
  ],
  "registryDependencies": [
    "@dwellir/dwellir-theme",
    "@dwellir/utils",
    "@dwellir/chart"
  ],
  "files": [
    {
      "path": "components/ui/chart-legend.tsx",
      "content": "'use client';\n\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport {\n  type ComponentProps,\n  type FocusEvent,\n  type MouseEvent,\n  type ReactNode,\n  useMemo,\n  useState,\n} from 'react';\nimport { ChartIndicator } from '@/components/ui/chart';\nimport {\n  assignSeriesColors,\n  CHART_SERIES_COLORS,\n  type ChartSeriesColor,\n} from '@/components/ui/chart-series';\nimport { cn } from '@/lib/utils';\n\n/*\n * One series in a legend. `label` names it; `detail` is its secondary\n * metadata (a network, a node type) in muted, smaller text; `value` is its\n * count, right-aligned in rows. `icon` is a slot for the caller's own mark,\n * such as a chain icon, after the series swatch. `color` takes a theme\n * series (`chart-1` to `chart-5`) or a token (`var(--color-<key>)` inside a\n * ChartContainer); an entry without one takes a distinct series colour for\n * its key, as `assignSeriesColors` assigns it.\n */\nexport type ChartLegendEntry = {\n  key: string;\n  label: ReactNode;\n  detail?: ReactNode;\n  value?: ReactNode;\n  icon?: ReactNode;\n  color?: ChartSeriesColor | `var(--${string})`;\n};\n\n/*\n * Rows on a phone, a wrapping row from `sm` up (`auto`): a phone reader\n * scans a column of names with its counts down the right edge faster than\n * wrapped groups of uneven width. `rows` and `wrap` pin either shape.\n */\nconst chartLegendVariants = cva('text-sm', {\n  variants: {\n    layout: {\n      auto: 'flex flex-col sm:flex-row sm:flex-wrap sm:items-center sm:gap-x-2',\n      rows: 'flex flex-col',\n      wrap: 'flex flex-row flex-wrap items-center gap-x-2',\n    },\n  },\n  defaultVariants: { layout: 'auto' },\n});\n\nconst chartLegendItemVariants = cva('flex items-center gap-2 rounded-md px-2 py-1.5', {\n  variants: {\n    layout: {\n      auto: 'w-full sm:w-auto',\n      rows: 'w-full',\n      wrap: '',\n    },\n  },\n  defaultVariants: { layout: 'auto' },\n});\n\n/* The list item itself takes the row width, so it stays in the accessibility tree. */\nconst chartLegendEntryVariants = cva('flex', {\n  variants: {\n    layout: {\n      auto: 'w-full sm:w-auto',\n      rows: 'w-full',\n      wrap: '',\n    },\n  },\n  defaultVariants: { layout: 'auto' },\n});\n\n/* A legend row a thumb can hit: 44 px tall on touch layouts, calmer on wide screens. */\nconst INTERACTIVE_CLASSES =\n  'group min-h-11 cursor-pointer touch-manipulation text-left transition-[color,background-color,opacity] duration-150 ease-out outline-none select-none hover:bg-muted focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring motion-reduce:transition-none sm:min-h-9';\n\nconst isSeriesColor = (color: ChartLegendEntry['color']): color is ChartSeriesColor =>\n  color != null && (CHART_SERIES_COLORS as readonly string[]).includes(color);\n\n/* An entry colour as the swatch reads it: a theme series reads its --color-chart-N token. */\nfunction markerColor(entry: ChartLegendEntry, assigned: Map<string, ChartSeriesColor>) {\n  if (entry.color?.startsWith('var(')) return entry.color;\n  const series = entry.color ?? assigned.get(entry.key) ?? CHART_SERIES_COLORS[0];\n  return `var(--chart-series-${series.slice(-1)})`;\n}\n\n/* One entry's swatch, icon slot, muted label and detail, and its count. */\nfunction LegendEntryContent({\n  entry,\n  color,\n  dimmed,\n  highlighted,\n}: {\n  entry: ChartLegendEntry;\n  color: string;\n  dimmed: boolean;\n  highlighted: boolean;\n}) {\n  return (\n    <>\n      <ChartIndicator color={color} className={cn('size-2', dimmed && 'opacity-60')} />\n      {entry.icon ? (\n        <span\n          data-slot=\"chart-legend-icon\"\n          aria-hidden=\"true\"\n          className=\"flex shrink-0 items-center [&>img]:size-4 [&>svg]:size-4\"\n        >\n          {entry.icon}\n        </span>\n      ) : null}\n      <span data-slot=\"chart-legend-text\" className=\"flex min-w-0 items-baseline gap-1.5\">\n        <span\n          data-slot=\"chart-legend-label\"\n          className={cn(\n            'truncate text-muted-foreground group-hover:text-foreground group-focus-visible:text-foreground',\n            highlighted && 'text-foreground',\n          )}\n        >\n          {entry.label}\n        </span>\n        {entry.detail ? (\n          <span data-slot=\"chart-legend-detail\" className=\"truncate text-xs text-muted-foreground\">\n            {entry.detail}\n          </span>\n        ) : null}\n      </span>\n      {entry.value != null ? (\n        <span\n          data-slot=\"chart-legend-value\"\n          className=\"ml-auto pl-4 text-right text-foreground tabular-nums\"\n        >\n          {entry.value}\n        </span>\n      ) : null}\n    </>\n  );\n}\n\nconst isRowOfList = (row: Element, candidate: unknown): candidate is HTMLElement =>\n  candidate instanceof HTMLElement &&\n  candidate.dataset.slot === 'chart-legend-item' &&\n  row.closest('[data-slot=\"chart-legend-list\"]')?.contains(candidate) === true;\n\n/* The series key of this list's row that holds keyboard focus, if any does. */\nfunction focusedLegendKey(row: Element) {\n  const focused = document.activeElement;\n  return isRowOfList(row, focused) ? focused.dataset.key : undefined;\n}\n\n/*\n * Hover and focus share one cue. Pointer leave restores the focused row's\n * cue within this list. Blur clears it when focus leaves the list.\n */\nfunction legendHighlightHandlers(\n  entry: ChartLegendEntry,\n  highlightedKey: string | null | undefined,\n  onHighlight: (key: string | null) => void,\n) {\n  return {\n    onMouseEnter: () => onHighlight(entry.key),\n    onMouseLeave: (event: MouseEvent<HTMLButtonElement>) => {\n      if (event.currentTarget === document.activeElement) return;\n      const focusedKey = focusedLegendKey(event.currentTarget);\n      if (focusedKey) onHighlight(focusedKey);\n      else if (highlightedKey == null || highlightedKey === entry.key) onHighlight(null);\n    },\n    onFocus: () => onHighlight(entry.key),\n    onBlur: (event: FocusEvent<HTMLButtonElement>) => {\n      if (!isRowOfList(event.currentTarget, event.relatedTarget)) onHighlight(null);\n    },\n  };\n}\n\ntype ChartLegendListProps = Omit<ComponentProps<'ul'>, 'onSelect'> &\n  VariantProps<typeof chartLegendVariants> & {\n    entries: readonly ChartLegendEntry[];\n    /* Rows shown before the rest collapse into \"+N more\". All rows by default. */\n    maxVisible?: number;\n    /* The series key the chart highlights now; the others recede. */\n    highlightedKey?: string | null;\n    /* Fires on pointer hover and on keyboard focus alike, and with null when both leave. Either handler puts each row in the tab order, as a button. */\n    onHighlight?: (key: string | null) => void;\n    /* Picks the series when its row's button is pressed. */\n    onSelect?: (key: string) => void;\n  };\n\n/**\n * A chart legend the caller lays out beside or under the chart, free of the\n * chart's own box. Labels rest muted and take the foreground colour while\n * their series is highlighted, by hover or by focus, so keyboard readers\n * get the same cue as the pointer. A long list collapses after `maxVisible`\n * rows into a \"+N more\" row that expands it.\n */\nfunction ChartLegendList({\n  entries,\n  layout,\n  maxVisible,\n  highlightedKey,\n  onHighlight,\n  onSelect,\n  className,\n  ...props\n}: ChartLegendListProps) {\n  const [expanded, setExpanded] = useState(false);\n  const assigned = useMemo(() => {\n    const reserved = entries.flatMap((entry) => (isSeriesColor(entry.color) ? [entry.color] : []));\n    return assignSeriesColors(\n      entries.filter((entry) => !entry.color).map((entry) => entry.key),\n      reserved,\n    );\n  }, [entries]);\n  if (!entries.length) return null;\n  const limit = maxVisible != null && maxVisible >= 0 ? Math.floor(maxVisible) : entries.length;\n  const collapsible = entries.length > limit;\n  const visible = expanded || !collapsible ? entries : entries.slice(0, limit);\n  const hidden = entries.length - visible.length;\n  const itemClass = (interactive: boolean) =>\n    cn(chartLegendItemVariants({ layout }), interactive && INTERACTIVE_CLASSES);\n  return (\n    <ul\n      data-slot=\"chart-legend-list\"\n      className={cn('ui-chart-palette', chartLegendVariants({ layout }), className)}\n      {...props}\n    >\n      {visible.map((entry) => {\n        const content = (\n          <LegendEntryContent\n            entry={entry}\n            color={markerColor(entry, assigned)}\n            highlighted={highlightedKey === entry.key}\n            dimmed={highlightedKey != null && highlightedKey !== entry.key}\n          />\n        );\n        const highlight = onHighlight\n          ? legendHighlightHandlers(entry, highlightedKey, onHighlight)\n          : {};\n        const interactive = Boolean(onSelect || onHighlight);\n        return (\n          <li\n            key={entry.key}\n            data-slot=\"chart-legend-entry\"\n            className={chartLegendEntryVariants({ layout })}\n          >\n            {interactive ? (\n              <button\n                type=\"button\"\n                data-slot=\"chart-legend-item\"\n                data-key={entry.key}\n                className={itemClass(true)}\n                onClick={onSelect ? () => onSelect(entry.key) : undefined}\n                {...highlight}\n              >\n                {content}\n              </button>\n            ) : (\n              <span data-slot=\"chart-legend-item\" data-key={entry.key} className={itemClass(false)}>\n                {content}\n              </span>\n            )}\n          </li>\n        );\n      })}\n      {collapsible ? (\n        <li data-slot=\"chart-legend-entry\" className={chartLegendEntryVariants({ layout })}>\n          <button\n            type=\"button\"\n            data-slot=\"chart-legend-more\"\n            aria-expanded={expanded}\n            aria-label={expanded ? 'Show fewer series' : `Show ${hidden} more series`}\n            className={cn(itemClass(true), 'text-muted-foreground hover:text-foreground')}\n            onClick={() => setExpanded((value) => !value)}\n          >\n            {expanded ? 'Show less' : `+${hidden} more`}\n          </button>\n        </li>\n      ) : null}\n    </ul>\n  );\n}\n\nexport { ChartLegendList, chartLegendEntryVariants, chartLegendItemVariants, chartLegendVariants };\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}