{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "bump-chart",
  "type": "registry:component",
  "title": "Bump Chart",
  "description": "How a set of things ranked against each other over time.",
  "dependencies": [
    "@base-ui/react",
    "@tanstack/charts",
    "tailwind-variants"
  ],
  "registryDependencies": [],
  "cssVars": {
    "light": {
      "success": "oklch(0.696 0.17 162.48)",
      "warning": "oklch(0.769 0.188 70.08)",
      "info": "oklch(0.623 0.214 259.815)"
    },
    "dark": {
      "success": "oklch(0.696 0.17 162.48)",
      "warning": "oklch(0.769 0.188 70.08)",
      "info": "oklch(0.623 0.214 259.815)"
    }
  },
  "files": [
    {
      "path": "components/panelui/bump-chart.tsx",
      "type": "registry:component",
      "target": "@components/panelui/bump-chart.tsx",
      "content": "'use client';\n\n/**\n * BumpChart — how a set of things ranked against each other over time.\n *\n * ```tsx\n * <BumpChart data={standings} xDataKey=\"week\" defaultHighlight=\"harbour\">\n *   <BumpChart.Grid />\n *   <BumpChart.Line dataKey=\"harbour\" label=\"Harbour\" color=\"chart-3\" />\n *   <BumpChart.Line dataKey=\"northside\" label=\"Northside\" />\n *   <BumpChart.YAxis />\n *   <BumpChart.XAxis />\n *   <BumpChart.Labels />\n *   <BumpChart.Tooltip />\n * </BumpChart>\n * ```\n *\n * ## The same parts as the app\n *\n * The parts and their props are the app library's BumpChart, so a chart written\n * for one reads the same in the other. What differs is what the web changes: a\n * colour is named by the theme's role, a column is read by hovering, tapping or\n * dragging, and the arrow keys walk the columns. `Standings` is only here: a\n * page has the room for a table, and it never covers the lines it describes.\n *\n * ## Positions, not values\n *\n * The y-axis is the rank, so the rows are evenly spaced whatever the gap between\n * the underlying scores. Pass the ranks directly, or pass the scores with\n * `values=\"score\"` and each column is ranked for you, highest first.\n *\n * ## The parts describe, the root draws\n *\n * `Grid`, `Line`, the axes and `Tooltip` draw nothing themselves. Each one tells\n * the root what it wants, and the root builds one chart definition from them.\n * The margins are fixed rather than measured, so the names and the skeleton,\n * which are HTML over the drawing, know where every row is without waiting for\n * it to be drawn.\n */\nimport {\n  crosshair,\n  defineChart,\n  lineY,\n  ruleX,\n  ruleY,\n  type ChartCurve,\n  type ChartInteractionController,\n  type ChartMark,\n  type ChartMotionContext,\n  type ChartPoint,\n  type SceneNode,\n} from '@tanstack/charts';\nimport { motion } from '@tanstack/charts/motion';\nimport { Chart } from '@tanstack/charts/react/core';\nimport { scaleLinear } from '@tanstack/charts/scales/linear';\nimport { tooltip as chartTooltip } from '@tanstack/charts/tooltip';\nimport {\n  createContext,\n  forwardRef,\n  useCallback,\n  useContext,\n  useEffect,\n  useImperativeHandle,\n  useMemo,\n  useRef,\n  useState,\n  type ComponentProps,\n  type PointerEvent as ReactPointerEvent,\n  type ReactNode,\n} from 'react';\nimport { tv } from 'tailwind-variants';\nimport { mergeProps } from '@base-ui/react/merge-props';\nimport { useRender } from '@base-ui/react/use-render';\nimport type { ElementType as RenderElementType } from 'react';\n\n/** Draws a part as another element, or composes it with a component: `render={<a href=\"/\" />}`. */\ntype RenderProp<Tag extends RenderElementType> = { render?: useRender.ComponentProps<Tag>['render'] };\n\nexport type BumpChartStatus = 'loading' | 'ready';\nexport type BumpChartDatum = Record<string, string | number | null | undefined>;\n\n/** Ranks per series key, one entry per row. `null` where a series has none. */\nexport type BumpRanks = Record<string, (number | null)[]>;\n\n/** Which of the theme's roles a series is drawn in. */\nexport type BumpChartColor =\n  | 'primary'\n  | 'info'\n  | 'success'\n  | 'warning'\n  | 'destructive'\n  | 'chart-1'\n  | 'chart-2'\n  | 'chart-3'\n  | 'chart-4'\n  | 'chart-5';\n\nconst COLOR_VAR: Record<BumpChartColor, string> = {\n  primary: 'var(--primary)',\n  info: 'var(--info)',\n  success: 'var(--success)',\n  warning: 'var(--warning)',\n  destructive: 'var(--destructive)',\n  'chart-1': 'var(--chart-1)',\n  'chart-2': 'var(--chart-2)',\n  'chart-3': 'var(--chart-3)',\n  'chart-4': 'var(--chart-4)',\n  'chart-5': 'var(--chart-5)',\n};\n\nconst MUTED = 'var(--muted-foreground)';\n\nconst THEME = {\n  foreground: 'var(--foreground)',\n  muted: MUTED,\n  grid: 'var(--border)',\n  background: 'transparent',\n  palette: ['var(--chart-1)', 'var(--chart-2)', 'var(--chart-3)', 'var(--chart-4)', 'var(--chart-5)'],\n};\n\nconst bumpChartVariants = tv({\n  slots: {\n    root: 'w-full text-foreground @container',\n    // Beside the chart where there is room for the table, under it where there is not.\n    layout:\n      'relative grid w-full grid-cols-1 @md:has-[[data-slot=bump-chart-standings]]:grid-cols-[minmax(0,1fr)_9.5rem] @md:has-[[data-slot=bump-chart-standings]]:gap-x-3',\n    plot: 'relative col-start-1 row-start-2 w-full',\n    // `pan-y` hands a vertical drag to the page and a sideways one to the chart.\n    chart:\n      'absolute! inset-0 touch-pan-y rounded-lg text-xs [--ts-chart-tooltip-background:var(--popover)] [--ts-chart-tooltip-border-radius:var(--radius-xl)] [--ts-chart-tooltip-border:1px_solid_var(--border)] [--ts-chart-tooltip-color:var(--popover-foreground)] [--ts-chart-tooltip-shadow:var(--shadow-lg)] [&_svg]:rounded-lg [&_svg]:outline-none [&_svg:focus-visible]:ring-2 [&_svg:focus-visible]:ring-ring',\n    sweepClip: 'pointer-events-none absolute inset-0 overflow-hidden',\n    sweep:\n      'absolute inset-y-0 left-0 w-2/5 bg-[linear-gradient(90deg,transparent,color-mix(in_oklab,var(--chart-1)_55%,transparent),transparent)] motion-reduce:hidden',\n    skeletonRow: 'absolute h-1 -translate-y-1/2 rounded-full bg-muted',\n    labels: 'pointer-events-none absolute inset-0 col-start-1 row-start-2',\n    label:\n      'pointer-events-auto absolute left-0 top-0 flex h-5 items-center truncate rounded-sm text-left text-xs outline-none transition-[transform,color] ease-in-out focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none',\n    standings: 'relative col-start-1 row-start-3 mt-3 w-full @md:col-start-2 @md:row-start-2 @md:mt-0',\n    standingsRow:\n      'absolute inset-x-0 top-0 grid h-[30px] grid-cols-[1.5rem_0.5rem_minmax(0,1fr)_auto] items-center gap-1.5 border-t border-border text-left text-xs outline-none transition-transform ease-in-out focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring motion-reduce:transition-none',\n    standingsRank: 'text-muted-foreground tabular-nums',\n    standingsMove: 'tabular-nums text-muted-foreground',\n    legend:\n      'pointer-events-none absolute right-2 top-1 col-start-1 row-start-2 flex gap-3 text-xs text-muted-foreground',\n    legendItem: 'flex items-center gap-1.5',\n    legendSwatch: 'size-2 shrink-0 rounded-full',\n    header: 'col-span-full col-start-1 row-start-1 flex items-start justify-between gap-3 pb-3',\n    headerText: 'flex min-w-0 flex-1 flex-col gap-0.5',\n    headerMuted: 'text-xs text-muted-foreground',\n    headerValue: 'text-xl font-bold tabular-nums tracking-tight',\n    headerTrailing: 'flex shrink flex-wrap items-center justify-end gap-x-3 gap-y-1 pt-1 text-xs text-muted-foreground',\n  },\n});\n\n/** Space round the plot, in pixels. */\nconst PADDING = { top: 8, right: 10, bottom: 32, left: 10 };\n\n/** Left margin kept for `#1`…`#12`, gutter included. */\nconst Y_AXIS_WIDTH = 42;\n\n/** Gap between the last column and the names. */\nconst LABELS_GAP = 12;\n\nconst DEFAULT_LABELS_WIDTH = 84;\n\n/** Margin on every side of a chart with no axes, which keeps the dots whole. */\nconst COMPACT_MARGIN = 6;\n\n/** How far the lines that are not picked out fade back. */\nconst DIMMED_OPACITY = 0.5;\n\n/** Height of a row of the standings. Their order is drawn by moving them, so it has to be known. */\nconst STANDINGS_ROW = 30;\n\n/** Rows to stand in for when there is no data yet to count them from. */\nconst SKELETON_ROWS = 4;\n\nconst SKELETON_THICKNESS = 4;\n\n/*\n * The lines are drawn in from the left. Only the lines: the axes and the guides\n * are already there, and the clip is measured against the whole drawing so a\n * line that starts late is not revealed late as well.\n */\nconst STYLES = `@keyframes panelui-bump-sweep{from{transform:translateX(-100%)}to{transform:translateX(250%)}}\n@keyframes panelui-bump-reveal{from{clip-path:inset(0 100% 0 0) view-box}to{clip-path:inset(0 0 0 0) view-box}}\n@media (prefers-reduced-motion:no-preference){[data-slot=bump-chart-plot][data-revealing] .ts-chart__line{animation:panelui-bump-reveal var(--bump-reveal) ease-out both}}`;\n\nconst defaultRank = (rank: number) => '#' + rank;\n\n/** Horizontal tangents at both ends of a segment, which is what makes the S between two ranks. */\nconst BUMP_CURVE: ChartCurve = {\n  line: (points) =>\n    points\n      .map(([x, y], index) => {\n        if (index === 0) return `M${x},${y}`;\n        const [previousX, previousY] = points[index - 1]!;\n        const middle = (previousX + x) / 2;\n        return `C${middle},${previousY} ${middle},${y} ${x},${y}`;\n      })\n      .join(''),\n  // A line never asks for this, and the type has no way to leave it out.\n  area: () => '',\n};\n\nconst finite = (value: BumpChartDatum[string]) =>\n  typeof value === 'number' && Number.isFinite(value) ? value : undefined;\n\n/**\n * Each series' rank in every row.\n *\n * With `values: 'rank'` the rows already hold ranks and they are only\n * validated. With `values: 'score'` each row is ranked highest first; a tie\n * keeps the order the keys are passed in, so two equal scores never draw on\n * top of each other.\n */\nexport function bumpRanks(\n  data: readonly BumpChartDatum[],\n  keys: readonly string[],\n  options: { values?: 'rank' | 'score' } = {}\n): BumpRanks {\n  const ranks: BumpRanks = Object.fromEntries(keys.map((key) => [key, [] as (number | null)[]]));\n  for (const row of data) {\n    if (options.values === 'score') {\n      const scored = keys\n        .flatMap((key, order) => {\n          const score = finite(row[key]);\n          return score === undefined ? [] : [{ key, order, score }];\n        })\n        .sort((a, b) => b.score - a.score || a.order - b.order);\n      const place = new Map(scored.map(({ key }, index) => [key, index + 1]));\n      for (const key of keys) ranks[key]!.push(place.get(key) ?? null);\n    } else {\n      for (const key of keys) {\n        const rank = finite(row[key]);\n        ranks[key]!.push(rank !== undefined && rank >= 1 ? rank : null);\n      }\n    }\n  }\n  return ranks;\n}\n\n/** One point of a line: a series' rank in one column. */\ninterface BumpRow {\n  index: number;\n  series: string;\n  rank: number | null;\n}\n\ninterface SeriesEntry {\n  key: string;\n  color: string;\n  label: string;\n  strokeWidth: number;\n  showDots: boolean;\n}\n\n/**\n * What the parts have asked for. An axis formatter is part of the drawing, so a new one redraws it.\n * The readout's are read through `latest` when it opens, so a new one costs nothing.\n */\ninterface Slots {\n  grid?: { vertical: boolean; horizontal: boolean; dashArray: string; opacity: number };\n  xAxis?: Pick<BumpChartXAxisProps, 'ticks' | 'format'>;\n  yAxis?: Pick<BumpChartYAxisProps, 'format'>;\n  labels?: { width: number };\n  standings?: true;\n  tooltip?: { className: string | undefined; latest: { current: Pick<BumpChartTooltipProps, 'formatRank' | 'formatX'> } };\n  skeleton?: { rows: number | undefined; duration: number };\n}\n\ninterface BumpChartContextValue {\n  data: readonly BumpChartDatum[];\n  xDataKey: string;\n  series: readonly SeriesEntry[];\n  setSeries: (entry: SeriesEntry) => void;\n  removeSeries: (key: string) => void;\n  setSlot: <K extends keyof Slots>(slot: K, value: Slots[K]) => void;\n  status: BumpChartStatus;\n  ranks: BumpRanks;\n  /** Where a rank is drawn, in pixels from the top of the plot. */\n  rowY: (rank: number) => number;\n  /** The drawn box of the plot, in pixels. */\n  box: { left: number; top: number; width: number; height: number };\n  morphDuration: number;\n  highlight: string | null;\n  setHighlight: (key: string | null) => void;\n  activeIndex: number;\n}\n\nconst BumpChartContext = createContext<BumpChartContextValue | null>(null);\n\nfunction useChart(component: string): BumpChartContextValue {\n  const context = useContext(BumpChartContext);\n  if (!context) {\n    throw new Error(`${component} must be used within a <BumpChart>`);\n  }\n  return context;\n}\n\n/**\n * The column being read and the line picked out, for something rendered inside\n * the chart. A readout outside it takes `onActiveIndexChange` instead.\n */\nexport function useBumpChart() {\n  const { data, activeIndex, xDataKey, ranks, highlight, setHighlight } = useChart('useBumpChart');\n  return {\n    activeIndex,\n    activePoint: activeIndex >= 0 ? (data[activeIndex] ?? null) : null,\n    xDataKey,\n    ranks,\n    highlight,\n    setHighlight,\n  };\n}\n\n/** The dots have one size and no opacity of their own on the mark, so they are set once placed. */\nfunction withDots<TMark extends ChartMark<BumpRow, number, number, number, number>>(\n  mark: TMark,\n  { radius, opacity }: { radius: number; opacity: number }\n): TMark {\n  const adjust = (node: SceneNode): SceneNode => {\n    if (node.kind === 'group') return { ...node, children: node.children.map(adjust) };\n    if (node.kind !== 'dot') return node;\n    return { ...node, radius, style: { ...node.style, fillOpacity: opacity } };\n  };\n  return {\n    ...mark,\n    initialize(context) {\n      const initialized = mark.initialize(context);\n      return {\n        ...initialized,\n        postDomain(scene) {\n          const base = initialized.postDomain ? initialized.postDomain(scene) : scene;\n          return { ...base, nodes: base.nodes.map(adjust) };\n        },\n      };\n    },\n  };\n}\n\nexport interface BumpChartProps extends Omit<ComponentProps<'div'>, 'ref'> {\n  /** The rows. Each one is a column: a week, a round, a release. */\n  data: readonly BumpChartDatum[];\n  /** Key holding the column's label. */\n  xDataKey?: string;\n  /**\n   * What the series columns hold. `rank` takes them as places, `1` at the top.\n   * `score` ranks every row for you, highest first.\n   */\n  values?: 'rank' | 'score';\n  /**\n   * The series drawn in its colour and on top, with the rest muted. `null` picks\n   * none, and every line keeps its own colour. Controlled: pair it with\n   * `onHighlightChange`.\n   */\n  highlight?: string | null;\n  /** The series picked out on first render, when `highlight` is not passed. */\n  defaultHighlight?: string | null;\n  /** Called when a name in `Labels` or a row of `Standings` is pressed. */\n  onHighlightChange?: (key: string | null) => void;\n  /**\n   * `loading` hides the lines and shows `BumpChart.Skeleton` if there is one.\n   * The lines are drawn in from the left when it turns `ready`.\n   */\n  status?: BumpChartStatus;\n  /** The plot's width ÷ height. */\n  aspectRatio?: number;\n  /** Milliseconds for the lines to draw in. */\n  animationDuration?: number;\n  /** Milliseconds for the lines to move to new places when the data changes. */\n  morphDuration?: number;\n  /**\n   * The column being read, and `-1`/`null` when there is none. Fires when the\n   * index changes, not per frame.\n   */\n  onActiveIndexChange?: (index: number, datum: BumpChartDatum | null) => void;\n  /** Drop the axis padding, for a chart with no axes. */\n  compact?: boolean;\n  /** What a screen reader calls the chart. */\n  label?: string;\n  /** What a screen reader says for one column. Defaults to its label and every series' rank. */\n  labelForDatum?: (datum: BumpChartDatum, index: number) => string;\n}\n\n/** Imperative handle: re-run the draw-in, for a \"replay\" control. */\nexport interface BumpChartHandle {\n  replay: () => void;\n}\n\nconst BumpChartRoot = forwardRef<BumpChartHandle, BumpChartProps & RenderProp<'div'>>(function BumpChartRoot(\n  {\n    data,\n    xDataKey = 'date',\n    values = 'rank',\n    highlight: highlightProp,\n    defaultHighlight = null,\n    onHighlightChange,\n    status = 'ready',\n    aspectRatio = 1.7,\n    animationDuration = 700,\n    morphDuration = 500,\n    onActiveIndexChange,\n    compact = false,\n    label,\n    labelForDatum,\n    className,\n    children,\n    render,\n    ...props\n  },\n  ref\n) {\n  const slotClasses = bumpChartVariants();\n  const plotRef = useRef<HTMLDivElement>(null);\n  const [width, setWidth] = useState(0);\n  const [series, setSeriesState] = useState<readonly SeriesEntry[]>([]);\n  const [slots, setSlots] = useState<Slots>({});\n  const [activeIndex, setActiveIndex] = useState(-1);\n  const [revision, setRevision] = useState(0);\n  const [revealedKey, setRevealedKey] = useState('');\n  const [ownHighlight, setOwnHighlight] = useState(defaultHighlight);\n  const interactionRef = useRef<ChartInteractionController<BumpRow, number, number> | null>(null);\n  const dragging = useRef(false);\n\n  const highlight = highlightProp === undefined ? ownHighlight : highlightProp;\n  const setHighlight = useCallback(\n    (key: string | null) => {\n      setOwnHighlight(key);\n      onHighlightChange?.(key);\n    },\n    [onHighlightChange]\n  );\n\n  /*\n   * The chart mounts once its width is known. Mounted at a guess, the first\n   * measurement is a resize, and a resize ends the entrance before it is seen.\n   */\n  useEffect(() => {\n    const node = plotRef.current;\n    if (!node) return;\n    const read = () => setWidth(node.clientWidth);\n    read();\n    const observer = new ResizeObserver(read);\n    observer.observe(node);\n    return () => observer.disconnect();\n  }, []);\n\n  const setSeries = useCallback(\n    (entry: SeriesEntry) =>\n      setSeriesState((current) =>\n        current.some(({ key }) => key === entry.key)\n          ? current.map((existing) => (existing.key === entry.key ? entry : existing))\n          : [...current, entry]\n      ),\n    []\n  );\n  const removeSeries = useCallback(\n    (key: string) => setSeriesState((current) => current.filter((entry) => entry.key !== key)),\n    []\n  );\n  const setSlot = useCallback<BumpChartContextValue['setSlot']>(\n    (slot, value) => setSlots((current) => ({ ...current, [slot]: value })),\n    []\n  );\n\n  useImperativeHandle(ref, () => ({ replay: () => setRevision((current) => current + 1) }), []);\n\n  const loading = status === 'loading';\n  const { grid, xAxis, yAxis, labels, standings, tooltip, skeleton } = slots;\n\n  const seriesKeys = series.map(({ key }) => key).join('\\n');\n  const ranks = useMemo(\n    () => bumpRanks(data, seriesKeys ? seriesKeys.split('\\n') : [], { values }),\n    [data, seriesKeys, values]\n  );\n\n  /** How many places there are: the lowest rank anybody holds. */\n  const dataRows = useMemo(\n    () => Math.max(1, ...Object.values(ranks).flatMap((column) => column.flatMap((rank) => rank ?? []))),\n    [ranks]\n  );\n  const rows = loading ? Math.max(1, skeleton?.rows ?? Math.max(dataRows, SKELETON_ROWS)) : dataRows;\n\n  const margin = compact\n    ? { top: COMPACT_MARGIN, right: COMPACT_MARGIN, bottom: COMPACT_MARGIN, left: COMPACT_MARGIN }\n    : {\n        ...PADDING,\n        left: yAxis ? Y_AXIS_WIDTH : PADDING.left,\n        right: labels ? labels.width + LABELS_GAP : PADDING.right,\n      };\n  const marginKey = `${margin.top}:${margin.right}:${margin.bottom}:${margin.left}`;\n\n  const interactive = (tooltip !== undefined || standings !== undefined) && !loading && data.length > 0;\n\n  const definition = useMemo(() => {\n    const columns = Array.from({ length: data.length }, (_unused, index) => index);\n    const places = Array.from({ length: rows }, (_unused, index) => index + 1);\n\n    const guides = grid\n      ? [\n          ...(grid.vertical\n            ? [ruleX(columns, { x: (index: number) => index, stroke: 'var(--border)', strokeOpacity: grid.opacity, strokeDasharray: grid.dashArray, motion: false as const })]\n            : []),\n          ...(grid.horizontal\n            ? [ruleY(places, { y: (place: number) => place, stroke: 'var(--border)', strokeOpacity: grid.opacity, strokeDasharray: grid.dashArray, motion: false as const })]\n            : []),\n        ]\n      : [];\n\n    // The picked line goes last, so it sits on top of every crossing.\n    const ordered = highlight\n      ? [...series.filter(({ key }) => key !== highlight), ...series.filter(({ key }) => key === highlight)]\n      : series;\n    const lines = ordered.map(({ key, color, strokeWidth, showDots }) => {\n      const picked = highlight === key;\n      const dimmed = highlight !== null && !picked;\n      const rowsOfLine: BumpRow[] = (ranks[key] ?? []).map((rank, index) => ({ index, series: key, rank }));\n      const opacity = dimmed ? DIMMED_OPACITY : 1;\n      return withDots(\n        lineY(rowsOfLine, {\n          id: `bump-line:${key}`,\n          key: 'index',\n          x: 'index',\n          y: 'rank',\n          curve: BUMP_CURVE,\n          points: showDots,\n          stroke: dimmed ? MUTED : color,\n          strokeOpacity: opacity,\n          strokeWidth: picked ? strokeWidth + 1 : strokeWidth,\n        }),\n        { radius: picked ? 3.5 : 2.5, opacity }\n      );\n    });\n\n    const count = Math.min(xAxis?.ticks ?? 4, columns.length);\n    const step = count > 1 ? (columns.length - 1) / (count - 1) : 0;\n    const tickValues = Array.from({ length: count }, (_unused, index) => Math.round(index * step));\n    const columnLabel = (index: number) => {\n      const datum = data[index];\n      if (!datum) return '';\n      return xAxis?.format ? xAxis.format(datum, index) : String(datum[xDataKey] ?? '');\n    };\n\n    const xOptions = {\n      // One column has no span to be spread over, so it stands in the middle.\n      scale: scaleLinear(columns.length > 1 ? [0, columns.length - 1] : [-1, 1], [0, 1]),\n      nice: false,\n      grid: false as const,\n      axis:\n        xAxis && !compact\n          ? { line: false as const, tickLabels: { opacity: 1 }, ticks: { size: 0, padding: 12, values: tickValues, format: columnLabel } }\n          : (false as const),\n    };\n    const yOptions = {\n      // Reversed, so the first place is at the top. One row stands in the middle.\n      scale: scaleLinear(rows > 1 ? [rows, 1] : [2, 0], [0, 1]),\n      nice: false,\n      grid: false as const,\n      axis:\n        yAxis && !compact\n          ? {\n              line: false as const,\n              tickLabels: { opacity: 1 },\n              ticks: {\n                size: 0,\n                padding: 12,\n                values: places,\n                format: (place: number) => (loading ? '' : (yAxis.format ?? defaultRank)(place)),\n              },\n            }\n          : (false as const),\n    };\n\n    const readout = tooltip\n      ? {\n          use: chartTooltip,\n          className: tooltip.className,\n          content(points: readonly ChartPoint<BumpRow, number, number>[]) {\n            const { formatRank = defaultRank, formatX } = tooltip.latest.current;\n            const column = points[0]?.datum.index;\n            const datum = column === undefined ? undefined : data[column];\n            if (column === undefined || !datum) return { title: '', rows: [] };\n            return {\n              title: formatX ? formatX(datum) : String(datum[xDataKey] ?? ''),\n              // The standings at this column. They come from the ranks because the\n              // focus only hands over the line under the pointer.\n              rows: series\n                .flatMap((entry) => {\n                  const rank = ranks[entry.key]?.[column];\n                  return rank === null || rank === undefined ? [] : [{ entry, rank }];\n                })\n                .sort((a, b) => a.rank - b.rank)\n                .map(({ entry, rank }) => ({\n                  active: entry.key === highlight,\n                  label: entry.label,\n                  value: formatRank(rank),\n                  color: drawnColor(entry, highlight),\n                })),\n            };\n          },\n        }\n      : undefined;\n\n    return defineChart({\n      theme: THEME,\n      marks: [\n        ...guides,\n        ...(loading ? [] : lines),\n        ...(interactive ? [crosshair({ y: false, stroke: 'var(--foreground)', strokeOpacity: 0.45, strokeWidth: 1 })] : []),\n      ],\n      scales: { x: xOptions, y: yOptions },\n      margin,\n      ...(compact ? { guides: false as const } : {}),\n      focusRing: false,\n      // The lines arrive by the clip in `STYLES`, and everything else is simply there.\n      motion: (context: ChartMotionContext<BumpRow>) =>\n        context.phase === 'enter'\n          ? (false as const)\n          : { transition: { type: 'tween' as const, duration: morphDuration, easing: 'ease-in-out' as const } },\n      keyboard: interactive,\n      pointer: interactive,\n      ...(interactive\n        ? {\n            focus: 'group-x' as const,\n            maxFocusDistance: Number.POSITIVE_INFINITY,\n            ...(readout ? { tooltip: readout } : {}),\n          }\n        : {}),\n    });\n  }, [data, xDataKey, series, ranks, rows, loading, highlight, compact, morphDuration, interactive, grid, xAxis, yAxis, tooltip, marginKey]);\n\n  // Marks take their timing from the definition. This one is what the readout\n  // moves with, and it should keep up with the pointer.\n  const renderer = useMemo(() => motion({ transition: { type: 'tween', duration: 160, easing: 'ease-out' } }), []);\n\n  const box = useMemo(() => {\n    const height = width / aspectRatio;\n    return {\n      left: margin.left,\n      top: margin.top,\n      width: Math.max(width - margin.left - margin.right, 0),\n      height: Math.max(height - margin.top - margin.bottom, 0),\n    };\n    // The margin is rebuilt every render, so it is compared by value.\n  }, [width, aspectRatio, marginKey]);\n\n  const rowY = useCallback(\n    (rank: number) => box.top + (rows > 1 ? ((rank - 1) / (rows - 1)) * box.height : box.height / 2),\n    [box, rows]\n  );\n\n  const chartKey = `${revision}:${status}`;\n  const summary = label ?? `Bump chart, ${data.length} ${data.length === 1 ? 'column' : 'columns'}`;\n\n  const context = useMemo<BumpChartContextValue>(\n    () => ({\n      data,\n      xDataKey,\n      series,\n      setSeries,\n      removeSeries,\n      setSlot,\n      status,\n      ranks,\n      rowY,\n      box,\n      morphDuration,\n      highlight,\n      setHighlight,\n      activeIndex,\n    }),\n    [data, xDataKey, series, setSeries, removeSeries, setSlot, status, ranks, rowY, box, morphDuration, highlight, setHighlight, activeIndex]\n  );\n\n  const spokenRow = (datum: BumpChartDatum, index: number) =>\n    labelForDatum?.(datum, index) ??\n    [\n      String(datum[xDataKey] ?? ''),\n      ...series.flatMap(({ key, label: name }) => {\n        const rank = ranks[key]?.[index];\n        return rank === null || rank === undefined ? [] : [`${name}, rank ${rank}`];\n      }),\n    ].join('. ');\n\n  // The chart also reports a column again when the data is rebuilt under it. Only a\n  // change of column is news, and repeating it can loop a caller that keeps the datum.\n  const reportedIndex = useRef(-1);\n  const reportActive = (index: number) => {\n    if (reportedIndex.current === index) return;\n    reportedIndex.current = index;\n    setActiveIndex(index);\n    onActiveIndexChange?.(index, index >= 0 ? (data[index] ?? null) : null);\n  };\n  const onFocusChange = (point: ChartPoint<BumpRow, number, number> | null) =>\n    reportActive(point ? point.datum.index : -1);\n\n  // A remounted chart says nothing about the focus it lost.\n  useEffect(() => reportActive(-1), [revision, status]);\n\n  /*\n   * The chart follows a mouse on its own but not a finger, which it treats as a\n   * tap. A sideways drag reads the columns it crosses, and the last one stays until\n   * the next press, the same as a tap's. A vertical drag never gets here: `pan-y`\n   * gives it to the page, and the browser cancels the pointer.\n   */\n  const readDrag = (event: ReactPointerEvent<HTMLDivElement>) => {\n    if (event.pointerType !== 'touch' || !interactive) return;\n    const interaction = interactionRef.current;\n    const target = interaction?.resolvePointer(event.clientX, event.clientY);\n    if (!interaction || !target) return;\n    interaction.setControlledFocus(target);\n    if (dragging.current) return;\n    dragging.current = true;\n    document.addEventListener(\n      'pointerdown',\n      () => {\n        dragging.current = false;\n        interactionRef.current?.setControlledFocus(null);\n      },\n      { capture: true, once: true }\n    );\n  };\n\n  const stubs = loading && skeleton && box.width > 0;\n  const placesDrawn = Array.from({ length: rows }, (_unused, index) => index + 1);\n\n  const element = useRender({\n    defaultTagName: 'div',\n    render,\n    props: mergeProps(\n      {\n        'data-slot': 'bump-chart',\n        className: slotClasses.root({ className }),\n        children: (\n          <div className={slotClasses.layout()}>\n            <style>{STYLES}</style>\n            {loading ? null : (\n              <ul aria-label={summary} className=\"sr-only\">\n                {data.map((datum, index) => (\n                  <li key={index}>{spokenRow(datum, index)}</li>\n                ))}\n              </ul>\n            )}\n            <div\n              ref={plotRef}\n              data-slot=\"bump-chart-plot\"\n              data-revealing={!loading && revealedKey !== chartKey ? '' : undefined}\n              className={slotClasses.plot()}\n              style={{ aspectRatio, ['--bump-reveal' as string]: `${animationDuration}ms` }}\n              onAnimationEnd={(event) => {\n                if (event.animationName === 'panelui-bump-reveal') setRevealedKey(chartKey);\n              }}\n              onKeyDown={(event) => {\n                if (event.key === 'Escape') interactionRef.current?.setControlledFocus(null);\n              }}\n              onPointerMove={readDrag}\n              onPointerCancel={() => interactionRef.current?.setControlledFocus(null)}\n            >\n              {width > 0 ? (\n                <Chart\n                  // The draw-in belongs to a new chart: a load finishing, or a replay.\n                  key={chartKey}\n                  definition={definition}\n                  renderer={renderer}\n                  ariaLabel={summary}\n                  aspectRatio={aspectRatio}\n                  initialWidth={width}\n                  className={slotClasses.chart()}\n                  onFocusChange={onFocusChange}\n                  onRender={({ interaction }) => {\n                    interactionRef.current = interaction;\n                  }}\n                />\n              ) : null}\n              {stubs ? (\n                <div data-slot=\"bump-chart-skeleton\" aria-hidden>\n                  {placesDrawn.map((place) => (\n                    <span\n                      key={place}\n                      className={slotClasses.skeletonRow()}\n                      style={{ left: box.left, top: rowY(place), width: box.width }}\n                    />\n                  ))}\n                  <div\n                    className={slotClasses.sweepClip()}\n                    style={{\n                      clipPath: `path('${placesDrawn\n                        .map((place) => `M${box.left},${rowY(place) - SKELETON_THICKNESS / 2}h${box.width}v${SKELETON_THICKNESS}h${-box.width}Z`)\n                        .join('')}')`,\n                    }}\n                  >\n                    <div\n                      className={slotClasses.sweep()}\n                      style={{ animation: `panelui-bump-sweep ${skeleton.duration}ms linear infinite` }}\n                    />\n                  </div>\n                </div>\n              ) : null}\n            </div>\n            {children}\n          </div>\n        ),\n      },\n      props\n    ),\n  });\n\n  return <BumpChartContext.Provider value={context}>{element}</BumpChartContext.Provider>;\n});\nBumpChartRoot.displayName = 'BumpChart';\n\n/** Hands a part's formatters to the root without a new function rebuilding the chart. */\nfunction useLatest<T>(value: T) {\n  const [latest] = useState(() => ({ current: value }));\n  useEffect(() => {\n    latest.current = value;\n  });\n  return latest;\n}\n\nexport interface BumpChartGridProps {\n  /** A guide down every column. */\n  vertical?: boolean;\n  /** A guide across every rank. */\n  horizontal?: boolean;\n  /** The stroke's dash pattern. */\n  dashArray?: string;\n  /** Opacity of the lines. */\n  opacity?: number;\n}\n\n/** Guides down each column, so a dot can be read against the label below it. */\nfunction BumpChartGrid({ vertical = true, horizontal = false, dashArray = '3 5', opacity = 1 }: BumpChartGridProps) {\n  const { setSlot } = useChart('BumpChart.Grid');\n  useEffect(() => {\n    setSlot('grid', { vertical, horizontal, dashArray, opacity });\n    return () => setSlot('grid', undefined);\n  }, [vertical, horizontal, dashArray, opacity, setSlot]);\n  return null;\n}\nBumpChartGrid.displayName = 'BumpChart.Grid';\n\nexport interface BumpChartLineProps {\n  /** Column in the data holding this series' rank, or its score with `values=\"score\"`. */\n  dataKey: string;\n  /** The name shown by `Labels`, `Standings`, the tooltip and the legend. Defaults to `dataKey`. */\n  label?: string;\n  /** Which of the theme's roles the series is drawn in. */\n  color?: BumpChartColor;\n  /** Thickness of the line. The picked-out line is drawn one pixel thicker. */\n  strokeWidth?: number;\n  /** Draw a dot at every column. */\n  showDots?: boolean;\n}\n\n/**\n * One series. It draws nothing itself: the lines are drawn together, so the one\n * picked out can be drawn last and sit on top of every crossing.\n */\nfunction BumpChartLine({ dataKey, label, color = 'chart-1', strokeWidth = 1.5, showDots = true }: BumpChartLineProps) {\n  const { setSeries, removeSeries } = useChart('BumpChart.Line');\n  const stroke = COLOR_VAR[color];\n  const name = label ?? dataKey;\n  useEffect(() => {\n    setSeries({ key: dataKey, color: stroke, label: name, strokeWidth, showDots });\n  }, [dataKey, stroke, name, strokeWidth, showDots, setSeries]);\n  // Apart from the update above, so a new colour keeps the series' place in the order.\n  useEffect(() => () => removeSeries(dataKey), [dataKey, removeSeries]);\n  return null;\n}\nBumpChartLine.displayName = 'BumpChart.Line';\n\nexport interface BumpChartSkeletonProps {\n  /** How many rank rows to stand in for. Defaults to the data's, and at least four. */\n  rows?: number;\n  /** Milliseconds for one pass of the sweep. */\n  duration?: number;\n}\n\n/**\n * The loading state: a thin bar on every rank row with a highlight travelling\n * across them. Flat, so it says where the lines will be and nothing about where\n * they go.\n *\n * The highlight is a child translated inside a clip of the bars' outlines, so\n * the loop runs on the compositor and repaints nothing.\n */\nfunction BumpChartSkeleton({ rows, duration = 1400 }: BumpChartSkeletonProps) {\n  const { setSlot } = useChart('BumpChart.Skeleton');\n  useEffect(() => {\n    setSlot('skeleton', { rows, duration });\n    return () => setSlot('skeleton', undefined);\n  }, [rows, duration, setSlot]);\n  return null;\n}\nBumpChartSkeleton.displayName = 'BumpChart.Skeleton';\n\nexport interface BumpChartXAxisProps {\n  /** How many columns to label, spread across the run. */\n  ticks?: number;\n  /** Turn a row into its label. Defaults to the value at `xDataKey`. */\n  format?: (datum: BumpChartDatum, index: number) => string;\n}\n\n/** The column labels along the bottom. */\nfunction BumpChartXAxis({ ticks, format }: BumpChartXAxisProps) {\n  const { setSlot } = useChart('BumpChart.XAxis');\n  useEffect(() => {\n    setSlot('xAxis', { ticks, format });\n    return () => setSlot('xAxis', undefined);\n  }, [ticks, format, setSlot]);\n  return null;\n}\nBumpChartXAxis.displayName = 'BumpChart.XAxis';\n\nexport interface BumpChartYAxisProps {\n  /** Format a rank. Defaults to `#1`, `#2`… */\n  format?: (rank: number) => string;\n}\n\n/** The ranks down the side, one on every row. */\nfunction BumpChartYAxis({ format }: BumpChartYAxisProps) {\n  const { setSlot } = useChart('BumpChart.YAxis');\n  useEffect(() => {\n    setSlot('yAxis', { format });\n    return () => setSlot('yAxis', undefined);\n  }, [format, setSlot]);\n  return null;\n}\nBumpChartYAxis.displayName = 'BumpChart.YAxis';\n\n/** The colour a line is drawn in right now, so a muted line has a muted swatch. */\nconst drawnColor = (entry: SeriesEntry, highlight: string | null) =>\n  highlight !== null && highlight !== entry.key ? MUTED : entry.color;\n\nexport interface BumpChartLabelsProps extends Omit<ComponentProps<'div'>, 'children'> {\n  /** Room kept to the right of the plot for the names. Longer names are cut short. */\n  width?: number;\n  /** Let pressing a name pick that line out, and pressing it again clear it. */\n  pressable?: boolean;\n}\n\n/**\n * Each series' name beside its last point, so the lines can be told apart\n * without a legend to look across to. The names move with their lines when the\n * data changes.\n */\nfunction BumpChartLabels({\n  width = DEFAULT_LABELS_WIDTH,\n  pressable = true,\n  className,\n  render,\n  ...props\n}: BumpChartLabelsProps & RenderProp<'div'>) {\n  const { data, series, ranks, rowY, box, status, morphDuration, highlight, setHighlight, setSlot } = useChart('BumpChart.Labels');\n  const slots = bumpChartVariants();\n\n  useEffect(() => {\n    setSlot('labels', { width });\n    return () => setSlot('labels', undefined);\n  }, [width, setSlot]);\n\n  const element = useRender({\n    defaultTagName: 'div',\n    render,\n    props: mergeProps(\n      {\n        'data-slot': 'bump-chart-labels',\n        className: slots.labels({ className }),\n        children: series.flatMap((entry) => {\n          const line = ranks[entry.key] ?? [];\n          const end = line.reduce<number>((found, rank, index) => (rank === null ? found : index), -1);\n          const last = line[end];\n          if (last === null || last === undefined) return [];\n          // A line that stops early keeps its name beside where it stopped, not at a rank another line holds.\n          const left = box.left + (data.length > 1 ? end / (data.length - 1) : 0.5) * box.width + LABELS_GAP;\n          const picked = highlight === entry.key;\n          const dimmed = highlight !== null && !picked;\n          const shared = {\n            className: slots.label({ className: picked ? 'font-semibold' : undefined }),\n            style: {\n              width,\n              // With nothing picked out, each name takes its line's colour, which\n              // is the only thing tying it to that line across the crossings.\n              color: picked ? 'var(--foreground)' : dimmed ? MUTED : entry.color,\n              transform: `translate(${left}px, ${rowY(last) - 10}px)`,\n              transitionDuration: `${morphDuration}ms`,\n            },\n          };\n          return pressable ? (\n            <button\n              key={entry.key}\n              type=\"button\"\n              aria-pressed={picked}\n              onClick={() => setHighlight(picked ? null : entry.key)}\n              {...shared}\n            >\n              <span className=\"truncate\">{entry.label}</span>\n            </button>\n          ) : (\n            <span key={entry.key} {...shared}>\n              <span className=\"truncate\">{entry.label}</span>\n            </span>\n          );\n        }),\n      },\n      props\n    ),\n  });\n\n  return status === 'loading' ? null : element;\n}\nBumpChartLabels.displayName = 'BumpChart.Labels';\n\nexport interface BumpChartStandingsProps extends Omit<ComponentProps<'div'>, 'children'> {\n  /** Format a rank. Defaults to `#1`, `#2`… */\n  formatRank?: (rank: number) => string;\n  /** Show the places gained or lost since the column before. */\n  movement?: boolean;\n  /** Let pressing a row pick that line out, and pressing it again clear it. */\n  pressable?: boolean;\n}\n\n/**\n * The table at the column being read, or at the last column when none is. It\n * goes beside the plot where there is room and under it where there is not, and\n * reading the chart reorders it. The rows keep their place in the DOM and are\n * moved with a transform, so keyboard focus does not jump as the order changes.\n */\nfunction BumpChartStandings({\n  formatRank = defaultRank,\n  movement = true,\n  pressable = true,\n  className,\n  render,\n  ...props\n}: BumpChartStandingsProps & RenderProp<'div'>) {\n  const { data, xDataKey, series, ranks, status, morphDuration, highlight, setHighlight, activeIndex, setSlot } =\n    useChart('BumpChart.Standings');\n  const slots = bumpChartVariants();\n\n  useEffect(() => {\n    setSlot('standings', true);\n    return () => setSlot('standings', undefined);\n  }, [setSlot]);\n\n  const column = activeIndex >= 0 ? activeIndex : data.length - 1;\n  const standing = series\n    .flatMap((entry) => {\n      const rank = ranks[entry.key]?.[column];\n      return rank === null || rank === undefined ? [] : [{ entry, rank }];\n    })\n    .sort((a, b) => a.rank - b.rank);\n  const position = new Map(standing.map(({ entry }, index) => [entry.key, index]));\n\n  const element = useRender({\n    defaultTagName: 'div',\n    render,\n    props: mergeProps(\n      {\n        'data-slot': 'bump-chart-standings',\n        role: 'group',\n        'aria-label': data[column] === undefined ? 'Standings' : `Standings, ${String(data[column][xDataKey] ?? `column ${column + 1}`)}`,\n        className: slots.standings({ className }),\n        style: { height: standing.length * STANDINGS_ROW },\n        children: standing.length\n          ? series.flatMap((entry) => {\n              const at = position.get(entry.key);\n              const rank = ranks[entry.key]?.[column];\n              if (at === undefined || rank === null || rank === undefined) return [];\n              const before = column > 0 ? ranks[entry.key]?.[column - 1] : null;\n              const moved = movement && before !== null && before !== undefined ? before - rank : 0;\n              const picked = highlight === entry.key;\n              const shared = {\n                className: slots.standingsRow({ className: at === 0 ? 'border-t-transparent' : undefined }),\n                style: { transform: `translateY(${at * STANDINGS_ROW}px)`, transitionDuration: `${morphDuration / 2}ms` },\n              };\n              const cells = (\n                <>\n                  <span className={slots.standingsRank()}>{formatRank(rank)}</span>\n                  <span className={slots.legendSwatch()} style={{ background: drawnColor(entry, highlight) }} />\n                  <span className={picked ? 'truncate font-semibold' : 'truncate'}>{entry.label}</span>\n                  <span className={slots.standingsMove()} style={moved > 0 ? { color: 'var(--success)' } : undefined}>\n                    {moved > 0 ? `+${moved}` : moved < 0 ? `−${-moved}` : ''}\n                  </span>\n                </>\n              );\n              return pressable ? (\n                <button\n                  key={entry.key}\n                  type=\"button\"\n                  aria-pressed={picked}\n                  onClick={() => setHighlight(picked ? null : entry.key)}\n                  {...shared}\n                >\n                  {cells}\n                </button>\n              ) : (\n                <div key={entry.key} {...shared}>\n                  {cells}\n                </div>\n              );\n            })\n          : null,\n      },\n      props\n    ),\n  });\n\n  return status === 'loading' ? null : element;\n}\nBumpChartStandings.displayName = 'BumpChart.Standings';\n\nexport interface BumpChartTooltipProps {\n  /** Format a rank in the readout. Defaults to `#1`, `#2`… */\n  formatRank?: (rank: number) => string;\n  /** Format the readout's heading from the row. Defaults to the value at xDataKey. */\n  formatX?: (datum: BumpChartDatum) => string;\n  /** Added to the readout's surface. */\n  className?: string;\n}\n\n/**\n * The readout, and what drives it: hovering a column, pressing or dragging across\n * them, or the arrow keys. It lists every series in the order it stood at that\n * column. The whole plot is the hit area, because a readout you have to land on a\n * thin line to summon is one nobody finds.\n */\nfunction BumpChartTooltip({ formatRank, formatX, className }: BumpChartTooltipProps) {\n  const { setSlot } = useChart('BumpChart.Tooltip');\n  const latest = useLatest({ formatRank, formatX });\n  useEffect(() => {\n    setSlot('tooltip', { className, latest });\n    return () => setSlot('tooltip', undefined);\n  }, [className, latest, setSlot]);\n  return null;\n}\nBumpChartTooltip.displayName = 'BumpChart.Tooltip';\n\nexport type BumpChartLegendProps = ComponentProps<'div'>;\n\n/** A swatch and a name per series, in the order the series were declared. */\nfunction BumpChartLegend({ className, render, ...props }: BumpChartLegendProps & RenderProp<'div'>) {\n  const { series, highlight } = useChart('BumpChart.Legend');\n  const slots = bumpChartVariants();\n\n  const element = useRender({\n    defaultTagName: 'div',\n    render,\n    props: mergeProps(\n      {\n        'data-slot': 'bump-chart-legend',\n        'aria-hidden': true,\n        className: slots.legend({ className }),\n        children: series.map((entry) => (\n          <span key={entry.key} className={slots.legendItem()}>\n            <span className={slots.legendSwatch()} style={{ background: drawnColor(entry, highlight) }} />\n            {entry.label}\n          </span>\n        )),\n      },\n      props\n    ),\n  });\n\n  return series.length ? element : null;\n}\nBumpChartLegend.displayName = 'BumpChart.Legend';\n\nexport interface BumpChartHeaderProps extends Omit<ComponentProps<'div'>, 'title'> {\n  /** Small line above the value: what the chart is of. */\n  title?: ReactNode;\n  /** The readout. The largest thing on the card, and the first thing read. */\n  value?: ReactNode;\n  /** One muted line under the value: a period, a comparison, a total. */\n  caption?: ReactNode;\n  /** Draw a swatch and a name per series along the trailing edge. */\n  legend?: boolean;\n  /** Trailing slot: a control, a badge, a range picker. Wins over `legend`. */\n  children?: ReactNode;\n}\n\n/**\n * The strip above the plot: what the chart is of and what it currently reads.\n * The value is not derived here. Pass the formatted string, from\n * `onActiveIndexChange` or `onHighlightChange` if it follows the chart.\n */\nfunction BumpChartHeader({\n  title,\n  value,\n  caption,\n  legend = false,\n  className,\n  children,\n  render,\n  ...props\n}: BumpChartHeaderProps & RenderProp<'div'>) {\n  const { series, highlight } = useChart('BumpChart.Header');\n  const slots = bumpChartVariants();\n  const trailing =\n    children ??\n    (legend && series.length\n      ? series.map((entry) => (\n          <span key={entry.key} className={slots.legendItem()}>\n            <span className={slots.legendSwatch()} style={{ background: drawnColor(entry, highlight) }} />\n            {entry.label}\n          </span>\n        ))\n      : null);\n\n  return useRender({\n    defaultTagName: 'div',\n    render,\n    props: mergeProps(\n      {\n        'data-slot': 'bump-chart-header',\n        className: slots.header({ className }),\n        children: (\n          <>\n            <div className={slots.headerText()}>\n              {title ? <div className={slots.headerMuted()}>{title}</div> : null}\n              {value ? <div className={slots.headerValue()}>{value}</div> : null}\n              {caption ? <div className={slots.headerMuted()}>{caption}</div> : null}\n            </div>\n            {trailing ? <div className={slots.headerTrailing()}>{trailing}</div> : null}\n          </>\n        ),\n      },\n      props\n    ),\n  });\n}\nBumpChartHeader.displayName = 'BumpChart.Header';\n\nexport const BumpChart = Object.assign(BumpChartRoot, {\n  Header: BumpChartHeader,\n  Grid: BumpChartGrid,\n  Line: BumpChartLine,\n  Skeleton: BumpChartSkeleton,\n  XAxis: BumpChartXAxis,\n  YAxis: BumpChartYAxis,\n  Labels: BumpChartLabels,\n  Standings: BumpChartStandings,\n  Tooltip: BumpChartTooltip,\n  Legend: BumpChartLegend,\n});\n\n/* By name as well, for a Server Component, which cannot dot into a client\n   module. */\nexport {\n  BumpChartHeader,\n  BumpChartGrid,\n  BumpChartLine,\n  BumpChartSkeleton,\n  BumpChartXAxis,\n  BumpChartYAxis,\n  BumpChartLabels,\n  BumpChartStandings,\n  BumpChartTooltip,\n  BumpChartLegend,\n};\n"
    }
  ]
}
