{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "chart-context",
  "type": "registry:component",
  "title": "Chart Context",
  "description": "Shared context and hooks for Bklit chart components",
  "dependencies": [
    "@visx/event@4.0.1-alpha.0",
    "@visx/responsive@4.0.1-alpha.0",
    "@visx/scale@4.0.1-alpha.0",
    "d3-array",
    "motion"
  ],
  "registryDependencies": [
    "@bklit/utils",
    "@bklit/chart-utils"
  ],
  "files": [
    {
      "path": "src/charts/chart-context.tsx",
      "content": "\"use client\";\n\nimport type { scaleBand, scaleLinear, scaleTime } from \"@visx/scale\";\n\ntype ScaleLinear<Output, _Input = number> = ReturnType<\n  typeof scaleLinear<Output>\n>;\ntype ScaleTime<Output, _Input = Date | number> = ReturnType<\n  typeof scaleTime<Output>\n>;\ntype ScaleBand<Domain extends { toString(): string }> = ReturnType<\n  typeof scaleBand<Domain>\n>;\n\nimport type { Transition } from \"motion/react\";\nimport {\n  createContext,\n  type Dispatch,\n  type ReactNode,\n  type RefObject,\n  type SetStateAction,\n  useContext,\n  useMemo,\n} from \"react\";\nimport type { ChartPhase, ChartStatus } from \"./chart-phase\";\nimport type { ReferenceAreaConfig } from \"./reference-area-config\";\nimport type { ChartSelection } from \"./use-chart-interaction\";\nimport { DEFAULT_Y_AXIS_ID } from \"./y-axis-scales\";\nimport type { YDomain } from \"./y-domain-utils\";\n\n// CSS variable references for theming\nexport const chartCssVars = {\n  background: \"var(--chart-background)\",\n  foreground: \"var(--chart-foreground)\",\n  foregroundMuted: \"var(--chart-foreground-muted)\",\n  label: \"var(--chart-label)\",\n  linePrimary: \"var(--chart-line-primary)\",\n  lineSecondary: \"var(--chart-line-secondary)\",\n  crosshair: \"var(--chart-crosshair)\",\n  grid: \"var(--chart-grid)\",\n  indicatorColor: \"var(--chart-indicator-color)\",\n  indicatorSecondaryColor: \"var(--chart-indicator-secondary-color)\",\n  markerBackground: \"var(--chart-marker-background)\",\n  markerBorder: \"var(--chart-marker-border)\",\n  markerForeground: \"var(--chart-marker-foreground)\",\n  badgeBackground: \"var(--chart-marker-badge-background)\",\n  badgeForeground: \"var(--chart-marker-badge-foreground)\",\n  segmentBackground: \"var(--chart-segment-background)\",\n  segmentLine: \"var(--chart-segment-line)\",\n  brushBorder: \"var(--chart-brush-border)\",\n  tooltipBackground: \"var(--chart-tooltip-background)\",\n};\n\n/** Default scatter series colors from the chart palette (`--chart-1` … `--chart-5`). */\nexport const defaultScatterColors = [\n  \"var(--chart-1)\",\n  \"var(--chart-2)\",\n  \"var(--chart-3)\",\n  \"var(--chart-4)\",\n  \"var(--chart-5)\",\n] as const;\n\nexport interface Margin {\n  top: number;\n  right: number;\n  bottom: number;\n  left: number;\n}\n\nexport interface TooltipData {\n  /** The data point being hovered */\n  point: Record<string, unknown>;\n  /** Index in the data array */\n  index: number;\n  /** X position in pixels (relative to chart area) */\n  x: number;\n  /** Y positions for each line, keyed by dataKey */\n  yPositions: Record<string, number>;\n  /** X positions for each series (for grouped bars), keyed by dataKey */\n  xPositions?: Record<string, number>;\n}\n\nexport interface LineConfig {\n  dataKey: string;\n  stroke: string;\n  strokeWidth: number;\n  /** Scale group id (Recharts `yAxisId`). Default: `\"left\"`. */\n  yAxisId?: string | number;\n}\n\n/**\n * Hover/selection state — every field here changes on mouse movement.\n * Lives in its own context so cold consumers (Grid, YAxis, PatternArea, …)\n * can subscribe to the stable slice and skip re-rendering on every hover.\n */\nexport interface ChartHoverContextValue {\n  // Tooltip state\n  tooltipData: TooltipData | null;\n  setTooltipData: Dispatch<SetStateAction<TooltipData | null>>;\n\n  // Selection state (optional - only present when useChartInteraction is used)\n  /** Current drag/pinch selection range */\n  selection?: ChartSelection | null;\n  /** Clear the current selection */\n  clearSelection?: () => void;\n\n  // Bar chart hover (optional - only present in BarChart)\n  /** Index of currently hovered bar */\n  hoveredBarIndex?: number | null;\n  /** Setter for hovered bar index */\n  setHoveredBarIndex?: (index: number | null) => void;\n\n  // Candlestick hover (optional - only present in CandlestickChart)\n  /** Index of currently hovered candle */\n  hoveredCandleIndex?: number | null;\n  /** Setter for hovered candle index */\n  setHoveredCandleIndex?: (index: number | null) => void;\n}\n\nexport interface ChartContextValue extends ChartHoverContextValue {\n  // Data\n  data: Record<string, unknown>[];\n  /** Decimated subset for SVG path rendering; equals `data` when no decimation is needed. */\n  renderData: Record<string, unknown>[];\n\n  // Scales\n  xScale: ScaleTime<number, number>;\n  /** Primary (left) y-scale — alias for `yScales[DEFAULT_Y_AXIS_ID]`. */\n  yScale: ScaleLinear<number, number>;\n  /** Per-axis y-scales keyed by `yAxisId`. */\n  yScales: Record<string, ScaleLinear<number, number>>;\n\n  // Dimensions\n  width: number;\n  height: number;\n  innerWidth: number;\n  innerHeight: number;\n  margin: Margin;\n\n  // Column width for spacing calculations\n  columnWidth: number;\n\n  // Container ref for portals\n  containerRef: RefObject<HTMLDivElement | null>;\n\n  // Line configurations (extracted from children)\n  lines: LineConfig[];\n\n  /** {@link ReferenceArea} bands — drives y-axis label colors in range. */\n  referenceAreas: ReferenceAreaConfig[];\n\n  // Loading / lifecycle (LineChart status transitions)\n  chartPhase: ChartPhase;\n  chartStatus: ChartStatus;\n  /** Centered label while `chartPhase` shows loading chrome. */\n  loadingLabel?: string;\n  /** Y-domain tween duration when transitioning loading ↔ ready (ms). */\n  yDomainTweenDuration: number;\n  /** Nice’d y-domains per axis from skeleton data (placeholder). */\n  yDomainSkeletonByAxis: Record<string, YDomain>;\n  /** Nice’d y-domains per axis from the current target data. */\n  yDomainTargetByAxis: Record<string, YDomain>;\n\n  // Animation state\n  isLoaded: boolean;\n  animationDuration: number;\n  /** CSS easing for clip-reveal / line draw (cartesian charts). */\n  animationEasing?: string;\n  /** Motion enter transition (spring or tween) — drives clip reveal when spring. */\n  enterTransition?: Transition;\n  /** Increments when enter animation should replay. */\n  revealEpoch?: number;\n  /** Fired when a one-shot loading pulse (exit / enter) completes. */\n  notifyLoadingPulseComplete?: () => void;\n\n  // X accessor - how to get the x value from data points\n  xAccessor: (d: Record<string, unknown>) => Date;\n\n  // Pre-computed date labels for ticker animation\n  dateLabels: string[];\n\n  /** Active brush zoom range — when set, axis ticks align to visible data rows. */\n  xDomain?: [Date, Date];\n  /** Full dataset length when brush zoom is enabled (for zoom vs full-range detection). */\n  xDomainSlotCount?: number;\n\n  // Bar chart specific (optional - only present in BarChart)\n  /** Band scale for categorical x-axis (bar charts) */\n  barScale?: ScaleBand<string>;\n  /** Width of each bar band */\n  bandWidth?: number;\n  /** X accessor for bar charts (returns string instead of Date) */\n  barXAccessor?: (d: Record<string, unknown>) => string;\n  /** Bar chart orientation */\n  orientation?: \"vertical\" | \"horizontal\";\n  /** Whether bars are stacked */\n  stacked?: boolean;\n  /** Stack offsets: Map of data index -> Map of dataKey -> cumulative offset */\n  stackOffsets?: Map<number, Map<string, number>>;\n  /** Squares variant — snap tooltip to top square and size ring dots. */\n  squareSnap?: { squareGap: number; groupGap?: number; fit?: boolean };\n\n  // ComposedChart + SeriesBar (optional)\n  /** `SeriesBar` dataKeys in tree order, for grouped columns at each x */\n  composedBarDataKeys?: string[];\n  /** Target bar width in px (Recharts `barSize` style). */\n  composedBarSize?: number;\n  /** Max bar width in px (Recharts `maxBarSize`). */\n  composedMaxBarSize?: number;\n  /** Gap between grouped `SeriesBar` columns in px. */\n  composedBarGap?: number;\n  /** When true, `SeriesBar` segments stack in child order at each x. */\n  composedStacked?: boolean;\n  /** Per-row cumulative offsets for stacked `SeriesBar` (data index → dataKey → offset). */\n  composedStackOffsets?: Map<number, Map<string, number>>;\n  /** Vertical gap in px between stacked `SeriesBar` segments. Default: 0 */\n  composedStackGap?: number;\n}\n\n/**\n * Stable slice of the chart context — everything that doesn't change on hover\n * (data, scales, dimensions, animation state, layout config). Consumers that\n * subscribe via `useChartStable()` skip re-renders on every mouse move.\n */\nexport type ChartStableContextValue = Omit<\n  ChartContextValue,\n  keyof ChartHoverContextValue\n>;\n\nconst ChartStableContext = createContext<ChartStableContextValue | null>(null);\nconst ChartHoverContext = createContext<ChartHoverContextValue | null>(null);\n\n/**\n * Splits the merged `value` into a stable slice and a volatile hover slice,\n * publishing each to its own context. Each slice is memoized on its own\n * field identities, so changing `tooltipData` does not bust the stable\n * slice — consumers of `useChartStable()` skip re-renders on hover.\n */\nexport function ChartProvider({\n  children,\n  value,\n}: {\n  children: ReactNode;\n  value: ChartContextValue;\n}) {\n  const stable = useMemo<ChartStableContextValue>(\n    () => ({\n      data: value.data,\n      renderData: value.renderData,\n      xScale: value.xScale,\n      yScale: value.yScale,\n      yScales: value.yScales,\n      width: value.width,\n      height: value.height,\n      innerWidth: value.innerWidth,\n      innerHeight: value.innerHeight,\n      margin: value.margin,\n      columnWidth: value.columnWidth,\n      containerRef: value.containerRef,\n      lines: value.lines,\n      referenceAreas: value.referenceAreas,\n      chartPhase: value.chartPhase,\n      chartStatus: value.chartStatus,\n      loadingLabel: value.loadingLabel,\n      yDomainTweenDuration: value.yDomainTweenDuration,\n      yDomainSkeletonByAxis: value.yDomainSkeletonByAxis,\n      yDomainTargetByAxis: value.yDomainTargetByAxis,\n      isLoaded: value.isLoaded,\n      animationDuration: value.animationDuration,\n      animationEasing: value.animationEasing,\n      enterTransition: value.enterTransition,\n      revealEpoch: value.revealEpoch,\n      notifyLoadingPulseComplete: value.notifyLoadingPulseComplete,\n      xAccessor: value.xAccessor,\n      dateLabels: value.dateLabels,\n      xDomain: value.xDomain,\n      xDomainSlotCount: value.xDomainSlotCount,\n      barScale: value.barScale,\n      bandWidth: value.bandWidth,\n      barXAccessor: value.barXAccessor,\n      orientation: value.orientation,\n      stacked: value.stacked,\n      stackOffsets: value.stackOffsets,\n      composedBarDataKeys: value.composedBarDataKeys,\n      composedBarSize: value.composedBarSize,\n      composedMaxBarSize: value.composedMaxBarSize,\n      composedBarGap: value.composedBarGap,\n      composedStacked: value.composedStacked,\n      composedStackOffsets: value.composedStackOffsets,\n      composedStackGap: value.composedStackGap,\n    }),\n    [\n      value.data,\n      value.renderData,\n      value.xScale,\n      value.yScale,\n      value.yScales,\n      value.width,\n      value.height,\n      value.innerWidth,\n      value.innerHeight,\n      value.margin,\n      value.columnWidth,\n      value.containerRef,\n      value.lines,\n      value.referenceAreas,\n      value.chartPhase,\n      value.chartStatus,\n      value.loadingLabel,\n      value.yDomainTweenDuration,\n      value.yDomainSkeletonByAxis,\n      value.yDomainTargetByAxis,\n      value.isLoaded,\n      value.animationDuration,\n      value.animationEasing,\n      value.enterTransition,\n      value.revealEpoch,\n      value.notifyLoadingPulseComplete,\n      value.xAccessor,\n      value.dateLabels,\n      value.xDomain,\n      value.xDomainSlotCount,\n      value.barScale,\n      value.bandWidth,\n      value.barXAccessor,\n      value.orientation,\n      value.stacked,\n      value.stackOffsets,\n      value.composedBarDataKeys,\n      value.composedBarSize,\n      value.composedMaxBarSize,\n      value.composedBarGap,\n      value.composedStacked,\n      value.composedStackOffsets,\n      value.composedStackGap,\n    ]\n  );\n\n  const hover = useMemo<ChartHoverContextValue>(\n    () => ({\n      tooltipData: value.tooltipData,\n      setTooltipData: value.setTooltipData,\n      selection: value.selection,\n      clearSelection: value.clearSelection,\n      hoveredBarIndex: value.hoveredBarIndex,\n      setHoveredBarIndex: value.setHoveredBarIndex,\n      hoveredCandleIndex: value.hoveredCandleIndex,\n      setHoveredCandleIndex: value.setHoveredCandleIndex,\n    }),\n    [\n      value.tooltipData,\n      value.setTooltipData,\n      value.selection,\n      value.clearSelection,\n      value.hoveredBarIndex,\n      value.setHoveredBarIndex,\n      value.hoveredCandleIndex,\n      value.setHoveredCandleIndex,\n    ]\n  );\n\n  return (\n    <ChartStableContext.Provider value={stable}>\n      <ChartHoverContext.Provider value={hover}>\n        {children}\n      </ChartHoverContext.Provider>\n    </ChartStableContext.Provider>\n  );\n}\n\n/**\n * Stable slice — data, scales, dimensions, animation state, layout config.\n * Subscribers skip re-renders on hover (the hover slice lives in a separate\n * context). Prefer this in cold consumers like axes, grid, pattern fills.\n */\nexport function useChartStable(): ChartStableContextValue {\n  const context = useContext(ChartStableContext);\n  if (!context) {\n    throw new Error(\n      \"useChartStable must be used within a ChartProvider. \" +\n        \"Make sure your component is wrapped in <LineChart>, <AreaChart>, <BarChart>, or <ComposedChart>.\"\n    );\n  }\n  return context;\n}\n\n/** Y-scale for a series axis (`yAxisId` on Line / Area / YAxis). */\nexport function useYScale(\n  yAxisId?: string | number\n): ScaleLinear<number, number> {\n  const { yScales, yScale } = useChartStable();\n  const id =\n    yAxisId == null || yAxisId === \"\" ? DEFAULT_Y_AXIS_ID : String(yAxisId);\n  return yScales[id] ?? yScale;\n}\n\n/**\n * Hover slice — tooltipData, selection, hovered bar / candle indices.\n * Subscribers re-render on every mouse move. Use only when the component\n * actually reads hover state.\n */\nexport function useChartHover(): ChartHoverContextValue {\n  const context = useContext(ChartHoverContext);\n  if (!context) {\n    throw new Error(\n      \"useChartHover must be used within a ChartProvider. \" +\n        \"Make sure your component is wrapped in <LineChart>, <AreaChart>, <BarChart>, or <ComposedChart>.\"\n    );\n  }\n  return context;\n}\n\n/**\n * Merged stable + hover context. Convenient for components that need both,\n * but re-renders on every hover (because hover changes). Prefer\n * `useChartStable()` or `useChartHover()` for hot consumers that only need\n * one slice.\n */\nexport function useChart(): ChartContextValue {\n  const stable = useChartStable();\n  const hover = useChartHover();\n  // Identity changes on every hover (hover is the volatile slice) — that's\n  // fine for consumers using this merged hook; they explicitly opted in to\n  // re-rendering on hover.\n  return { ...stable, ...hover };\n}\n\nexport default ChartStableContext;\n",
      "type": "registry:component",
      "target": "components/charts/chart-context.tsx"
    },
    {
      "path": "src/charts/reference-area-config.ts",
      "content": "import {\n  Children,\n  isValidElement,\n  type ReactElement,\n  type ReactNode,\n} from \"react\";\nimport { normalizeYAxisId } from \"./y-axis-scales\";\n\nexport interface ReferenceAreaConfig {\n  yAxisId: string;\n  y1?: number;\n  y2?: number;\n  axisLabelColor?: string;\n}\n\ninterface ReferenceAreaConfigProps {\n  yAxisId?: string | number;\n  y1?: number;\n  y2?: number;\n  axisLabelColor?: string;\n}\n\nfunction getChildComponentName(child: ReactElement) {\n  const childType = child.type as { displayName?: string; name?: string };\n  return typeof child.type === \"function\"\n    ? childType.displayName || childType.name || \"\"\n    : \"\";\n}\n\nfunction isReferenceAreaElement(child: ReactElement): boolean {\n  return getChildComponentName(child) === \"ReferenceArea\";\n}\n\n/** Collect {@link ReferenceArea} props from chart children for axis label styling. */\nexport function extractReferenceAreaConfigs(\n  children: ReactNode\n): ReferenceAreaConfig[] {\n  const configs: ReferenceAreaConfig[] = [];\n\n  const visit = (node: ReactNode) => {\n    Children.forEach(node, (child) => {\n      if (!isValidElement(child)) {\n        return;\n      }\n\n      if (isReferenceAreaElement(child)) {\n        const props = child.props as ReferenceAreaConfigProps | undefined;\n        if (props) {\n          configs.push({\n            yAxisId: normalizeYAxisId(props.yAxisId),\n            y1: props.y1,\n            y2: props.y2,\n            axisLabelColor: props.axisLabelColor,\n          });\n        }\n        return;\n      }\n\n      const childProps = child.props as { children?: ReactNode } | undefined;\n      if (childProps?.children) {\n        visit(childProps.children);\n      }\n    });\n  };\n\n  visit(children);\n  return configs;\n}\n",
      "type": "registry:lib",
      "target": "components/charts/reference-area-config.ts"
    },
    {
      "path": "src/charts/use-chart-interaction.ts",
      "content": "\"use client\";\n\nimport { localPoint } from \"@visx/event\";\nimport type { scaleLinear, scaleTime } from \"@visx/scale\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport type { LineConfig, Margin, TooltipData } from \"./chart-context\";\nimport { useScheduledTooltip } from \"./use-scheduled-tooltip\";\nimport { normalizeYAxisId } from \"./y-axis-scales\";\n\ntype ScaleTime = ReturnType<typeof scaleTime<number>>;\ntype ScaleLinear = ReturnType<typeof scaleLinear<number>>;\n\nexport interface ChartSelection {\n  startX: number;\n  endX: number;\n  startIndex: number;\n  endIndex: number;\n  active: boolean;\n}\n\ninterface UseChartInteractionParams {\n  xScale: ScaleTime;\n  yScale: ScaleLinear;\n  yScales: Record<string, ScaleLinear>;\n  data: Record<string, unknown>[];\n  lines: LineConfig[];\n  margin: Margin;\n  xAccessor: (d: Record<string, unknown>) => Date;\n  bisectDate: (\n    data: Record<string, unknown>[],\n    date: Date,\n    lo: number\n  ) => number;\n  canInteract: boolean;\n}\n\ninterface ChartInteractionResult {\n  tooltipData: TooltipData | null;\n  setTooltipData: React.Dispatch<React.SetStateAction<TooltipData | null>>;\n  selection: ChartSelection | null;\n  clearSelection: () => void;\n  interactionHandlers: {\n    onMouseMove?: (event: React.MouseEvent<SVGGElement>) => void;\n    onMouseLeave?: () => void;\n    onMouseDown?: (event: React.MouseEvent<SVGGElement>) => void;\n    onMouseUp?: () => void;\n    onTouchStart?: (event: React.TouchEvent<SVGGElement>) => void;\n    onTouchMove?: (event: React.TouchEvent<SVGGElement>) => void;\n    onTouchEnd?: () => void;\n  };\n  interactionStyle: React.CSSProperties;\n}\n\nexport function useChartInteraction({\n  xScale,\n  yScale,\n  yScales,\n  data,\n  lines,\n  margin,\n  xAccessor,\n  bisectDate,\n  canInteract,\n}: UseChartInteractionParams): ChartInteractionResult {\n  const [selection, setSelection] = useState<ChartSelection | null>(null);\n  const {\n    tooltipData,\n    setTooltipData,\n    scheduleTooltip,\n    clearTooltip,\n    resetTooltipDedupe,\n  } = useScheduledTooltip<TooltipData>();\n\n  const isDraggingRef = useRef(false);\n  const dragStartXRef = useRef<number>(0);\n  const lastHoveredXRef = useRef<number | null>(null);\n\n  const resolveTooltipFromX = useCallback(\n    (pixelX: number): TooltipData | null => {\n      const x0 = xScale.invert(pixelX);\n      const index = bisectDate(data, x0, 1);\n      const d0 = data[index - 1];\n      const d1 = data[index];\n\n      if (!d0) {\n        return null;\n      }\n\n      let d = d0;\n      let finalIndex = index - 1;\n      if (d1) {\n        const d0Time = xAccessor(d0).getTime();\n        const d1Time = xAccessor(d1).getTime();\n        if (x0.getTime() - d0Time > d1Time - x0.getTime()) {\n          d = d1;\n          finalIndex = index;\n        }\n      }\n\n      const yPositions: Record<string, number> = {};\n      for (const line of lines) {\n        const value = d[line.dataKey];\n        if (typeof value === \"number\") {\n          const axisScale = yScales[normalizeYAxisId(line.yAxisId)] ?? yScale;\n          yPositions[line.dataKey] = axisScale(value) ?? 0;\n        }\n      }\n\n      return {\n        point: d,\n        index: finalIndex,\n        x: xScale(xAccessor(d)) ?? 0,\n        yPositions,\n      };\n    },\n    [xScale, yScale, yScales, data, lines, xAccessor, bisectDate]\n  );\n\n  const resolveIndexFromX = useCallback(\n    (pixelX: number): number => {\n      const x0 = xScale.invert(pixelX);\n      const index = bisectDate(data, x0, 1);\n      const d0 = data[index - 1];\n      const d1 = data[index];\n      if (!d0) {\n        return 0;\n      }\n      if (d1) {\n        const d0Time = xAccessor(d0).getTime();\n        const d1Time = xAccessor(d1).getTime();\n        if (x0.getTime() - d0Time > d1Time - x0.getTime()) {\n          return index;\n        }\n      }\n      return index - 1;\n    },\n    [xScale, data, xAccessor, bisectDate]\n  );\n\n  const getChartX = useCallback(\n    (\n      event: React.MouseEvent<SVGGElement> | React.TouchEvent<SVGGElement>,\n      touchIndex = 0\n    ): number | null => {\n      let point: { x: number; y: number } | null = null;\n\n      if (\"touches\" in event) {\n        const touch = event.touches[touchIndex];\n        if (!touch) {\n          return null;\n        }\n        const svg = event.currentTarget.ownerSVGElement;\n        if (!svg) {\n          return null;\n        }\n        point = localPoint(svg, touch as unknown as MouseEvent);\n      } else {\n        point = localPoint(event);\n      }\n\n      if (!point) {\n        return null;\n      }\n      return point.x - margin.left;\n    },\n    [margin.left]\n  );\n\n  const handleMouseMove = useCallback(\n    (event: React.MouseEvent<SVGGElement>) => {\n      const chartX = getChartX(event);\n      if (chartX === null) {\n        return;\n      }\n\n      if (isDraggingRef.current) {\n        const startX = Math.min(dragStartXRef.current, chartX);\n        const endX = Math.max(dragStartXRef.current, chartX);\n        setSelection({\n          startX,\n          endX,\n          startIndex: resolveIndexFromX(startX),\n          endIndex: resolveIndexFromX(endX),\n          active: true,\n        });\n        return;\n      }\n\n      lastHoveredXRef.current = chartX;\n      const tooltip = resolveTooltipFromX(chartX);\n      if (tooltip) {\n        scheduleTooltip(tooltip);\n      }\n    },\n    [getChartX, resolveTooltipFromX, resolveIndexFromX, scheduleTooltip]\n  );\n\n  const handleMouseLeave = useCallback(() => {\n    lastHoveredXRef.current = null;\n    clearTooltip();\n    if (isDraggingRef.current) {\n      isDraggingRef.current = false;\n    }\n    setSelection(null);\n  }, [clearTooltip]);\n\n  const handleMouseDown = useCallback(\n    (event: React.MouseEvent<SVGGElement>) => {\n      const chartX = getChartX(event);\n      if (chartX === null) {\n        return;\n      }\n      isDraggingRef.current = true;\n      dragStartXRef.current = chartX;\n      clearTooltip();\n      setSelection(null);\n    },\n    [getChartX, clearTooltip]\n  );\n\n  const handleMouseUp = useCallback(() => {\n    if (isDraggingRef.current) {\n      isDraggingRef.current = false;\n    }\n    setSelection(null);\n  }, []);\n\n  const handleTouchStart = useCallback(\n    (event: React.TouchEvent<SVGGElement>) => {\n      if (event.touches.length === 1) {\n        event.preventDefault();\n        const chartX = getChartX(event, 0);\n        if (chartX === null) {\n          return;\n        }\n        lastHoveredXRef.current = chartX;\n        const tooltip = resolveTooltipFromX(chartX);\n        if (tooltip) {\n          scheduleTooltip(tooltip);\n        }\n      } else if (event.touches.length === 2) {\n        event.preventDefault();\n        resetTooltipDedupe();\n        clearTooltip();\n        const x0 = getChartX(event, 0);\n        const x1 = getChartX(event, 1);\n        if (x0 === null || x1 === null) {\n          return;\n        }\n        const startX = Math.min(x0, x1);\n        const endX = Math.max(x0, x1);\n        setSelection({\n          startX,\n          endX,\n          startIndex: resolveIndexFromX(startX),\n          endIndex: resolveIndexFromX(endX),\n          active: true,\n        });\n      }\n    },\n    [\n      getChartX,\n      resolveTooltipFromX,\n      resolveIndexFromX,\n      scheduleTooltip,\n      resetTooltipDedupe,\n      clearTooltip,\n    ]\n  );\n\n  const handleTouchMove = useCallback(\n    (event: React.TouchEvent<SVGGElement>) => {\n      if (event.touches.length === 1) {\n        event.preventDefault();\n        const chartX = getChartX(event, 0);\n        if (chartX === null) {\n          return;\n        }\n        lastHoveredXRef.current = chartX;\n        const tooltip = resolveTooltipFromX(chartX);\n        if (tooltip) {\n          scheduleTooltip(tooltip);\n        }\n      } else if (event.touches.length === 2) {\n        event.preventDefault();\n        const x0 = getChartX(event, 0);\n        const x1 = getChartX(event, 1);\n        if (x0 === null || x1 === null) {\n          return;\n        }\n        const startX = Math.min(x0, x1);\n        const endX = Math.max(x0, x1);\n        setSelection({\n          startX,\n          endX,\n          startIndex: resolveIndexFromX(startX),\n          endIndex: resolveIndexFromX(endX),\n          active: true,\n        });\n      }\n    },\n    [getChartX, resolveTooltipFromX, resolveIndexFromX, scheduleTooltip]\n  );\n\n  const handleTouchEnd = useCallback(() => {\n    clearTooltip();\n    setSelection(null);\n  }, [clearTooltip]);\n\n  const clearSelection = useCallback(() => {\n    setSelection(null);\n  }, []);\n\n  // Re-anchor tooltip/crosshair when x-scale or visible data changes (e.g. brush zoom commit).\n  useEffect(() => {\n    if (!canInteract || lastHoveredXRef.current === null) {\n      return;\n    }\n    const tooltip = resolveTooltipFromX(lastHoveredXRef.current);\n    if (tooltip) {\n      scheduleTooltip(tooltip, `${tooltip.index}:${Math.round(tooltip.x)}`);\n      return;\n    }\n    clearTooltip();\n  }, [canInteract, clearTooltip, resolveTooltipFromX, scheduleTooltip]);\n\n  const interactionHandlers = canInteract\n    ? {\n        onMouseMove: handleMouseMove,\n        onMouseLeave: handleMouseLeave,\n        onMouseDown: handleMouseDown,\n        onMouseUp: handleMouseUp,\n        onTouchStart: handleTouchStart,\n        onTouchMove: handleTouchMove,\n        onTouchEnd: handleTouchEnd,\n      }\n    : {};\n\n  const interactionStyle: React.CSSProperties = {\n    cursor: canInteract ? \"crosshair\" : \"default\",\n    touchAction: \"none\",\n  };\n\n  return {\n    tooltipData,\n    setTooltipData,\n    selection,\n    clearSelection,\n    interactionHandlers,\n    interactionStyle,\n  };\n}\n",
      "type": "registry:component",
      "target": "components/charts/use-chart-interaction.ts"
    },
    {
      "path": "src/charts/y-axis-scales.ts",
      "content": "import { scaleLinear } from \"@visx/scale\";\nimport type { LineConfig } from \"./chart-context\";\n\n/** Default axis id when `yAxisId` is omitted (Recharts-style `0` / primary left axis). */\nexport const DEFAULT_Y_AXIS_ID = \"left\";\n\nexport type YAxisOrientation = \"left\" | \"right\";\n\nexport function normalizeYAxisId(id?: string | number): string {\n  if (id == null || id === \"\") {\n    return DEFAULT_Y_AXIS_ID;\n  }\n  return String(id);\n}\n\nexport function groupLinesByYAxisId(\n  lines: LineConfig[]\n): Map<string, LineConfig[]> {\n  const groups = new Map<string, LineConfig[]>();\n  for (const line of lines) {\n    const axisId = normalizeYAxisId(line.yAxisId);\n    const bucket = groups.get(axisId) ?? [];\n    bucket.push(line);\n    groups.set(axisId, bucket);\n  }\n  return groups;\n}\n\ntype YScale = ReturnType<typeof scaleLinear<number>>;\n\nexport function getPrimaryYScale(\n  yScales: Record<string, YScale>,\n  fallback: YScale\n): YScale {\n  const primary = yScales[DEFAULT_Y_AXIS_ID];\n  if (primary) {\n    return primary;\n  }\n  const first = Object.values(yScales)[0];\n  return first ?? fallback;\n}\n\nexport function buildYScalesForLines({\n  lines,\n  innerHeight,\n  resolveDomain,\n}: {\n  lines: LineConfig[];\n  /** Passed by callers; domain is resolved via `resolveDomain`. */\n  data?: Record<string, unknown>[];\n  innerHeight: number;\n  resolveDomain: (dataKeys: string[]) => [number, number];\n}): Record<string, YScale> {\n  const groups = groupLinesByYAxisId(lines);\n  const scales: Record<string, YScale> = {};\n\n  for (const [axisId, axisLines] of groups) {\n    const dataKeys = axisLines.map((line) => line.dataKey);\n    const domain = resolveDomain(dataKeys);\n    scales[axisId] = scaleLinear({\n      range: [innerHeight, 0],\n      domain,\n      nice: true,\n    });\n  }\n\n  if (!scales[DEFAULT_Y_AXIS_ID]) {\n    scales[DEFAULT_Y_AXIS_ID] = scaleLinear({\n      range: [innerHeight, 0],\n      domain: [0, 100],\n      nice: true,\n    });\n  }\n\n  return scales;\n}\n\n/** Build y-scales from pre-computed (already nice'd) domain endpoints. */\nexport function buildYScalesFromDomains({\n  lines,\n  innerHeight,\n  domainsByAxis,\n}: {\n  lines: LineConfig[];\n  innerHeight: number;\n  domainsByAxis: Record<string, [number, number]>;\n}): Record<string, YScale> {\n  const groups = groupLinesByYAxisId(lines);\n  const scales: Record<string, YScale> = {};\n\n  for (const [axisId] of groups) {\n    const domain =\n      domainsByAxis[axisId] ??\n      domainsByAxis[DEFAULT_Y_AXIS_ID] ??\n      ([0, 100] as [number, number]);\n    scales[axisId] = scaleLinear({\n      range: [innerHeight, 0],\n      domain,\n    });\n  }\n\n  if (!scales[DEFAULT_Y_AXIS_ID]) {\n    scales[DEFAULT_Y_AXIS_ID] = scaleLinear({\n      range: [innerHeight, 0],\n      domain: domainsByAxis[DEFAULT_Y_AXIS_ID] ?? [0, 100],\n    });\n  }\n\n  return scales;\n}\n\n/** Single-axis charts (bar, scatter, candlestick, live line). */\nexport function wrapSingleYScale(yScale: YScale): Record<string, YScale> {\n  return { [DEFAULT_Y_AXIS_ID]: yScale };\n}\n",
      "type": "registry:lib",
      "target": "components/charts/y-axis-scales.ts"
    },
    {
      "path": "src/charts/y-axis-ticks.ts",
      "content": "/** Default hint passed to `scale.ticks()` (d3 — approximate tick count). */\nexport const Y_AXIS_DEFAULT_TICK_COUNT = 5;\n\n/** Minimum valid `numTicks` for `scale.ticks()` — values ≤ 0 yield no ticks. */\nexport const Y_AXIS_MIN_TICK_COUNT = 1;\n\n/**\n * Upper bound for the tick count hint. D3 may return more \"nice\" ticks above ~10;\n * keeping the hint in a modest range avoids overcrowded axes.\n */\nexport const Y_AXIS_MAX_TICK_COUNT = 10;\n\n/** Clamps a user `numTicks` value to a valid d3 tick-count hint. */\nexport function resolveYAxisTickCount(numTicks?: number): number {\n  if (numTicks == null || !Number.isFinite(numTicks)) {\n    return Y_AXIS_DEFAULT_TICK_COUNT;\n  }\n  const rounded = Math.round(numTicks);\n  if (rounded < Y_AXIS_MIN_TICK_COUNT) {\n    return Y_AXIS_MIN_TICK_COUNT;\n  }\n  if (rounded > Y_AXIS_MAX_TICK_COUNT) {\n    return Y_AXIS_MAX_TICK_COUNT;\n  }\n  return rounded;\n}\n",
      "type": "registry:lib",
      "target": "components/charts/y-axis-ticks.ts"
    },
    {
      "path": "src/charts/chart-phase.ts",
      "content": "/** Consumer-facing fetch / display status on time-series charts. */\nexport type ChartStatus = \"loading\" | \"ready\";\n\n/** Loading animation style: the default traveling pulse, or a diagonal\n * shimmer that sweeps across the skeleton. */\nexport type LoadingStyle = \"pulse\" | \"sweep\";\n\n/**\n * Internal visual lifecycle phase. Forward and reverse transitions add\n * intermediate phases in later stack branches.\n */\nexport type ChartPhase =\n  | \"loading\"\n  | \"exiting\"\n  | \"gridTweenReady\"\n  | \"revealing\"\n  | \"ready\"\n  | \"exitingReady\"\n  | \"gridTweenLoading\"\n  | \"revealingLoading\";\n\nexport const DEFAULT_CHART_STATUS: ChartStatus = \"ready\";\n\n/** Default Y-domain tween when transitioning loading ↔ ready (ms). */\nexport const DEFAULT_Y_DOMAIN_TWEEN_MS = 500;\n\n/** Relative domain delta below which Y tween may be skipped (see plan). */\nexport const Y_DOMAIN_TWEEN_SKIP_THRESHOLD = 0.02;\n\n/** Resting phase for a given status before transition orchestration runs. */\nexport function resolveRestingChartPhase(status: ChartStatus): ChartPhase {\n  return status === \"loading\" ? \"loading\" : \"ready\";\n}\n\nexport function isChartInteractionPhase(phase: ChartPhase): boolean {\n  return phase === \"ready\";\n}\n\nexport const DEFAULT_CHART_LIFECYCLE = {\n  chartPhase: \"ready\",\n  chartStatus: \"ready\",\n  loadingLabel: undefined,\n  yDomainTweenDuration: DEFAULT_Y_DOMAIN_TWEEN_MS,\n  yDomainSkeletonByAxis: { left: [0, 100] as [number, number] },\n  yDomainTargetByAxis: { left: [0, 100] as [number, number] },\n} as const satisfies {\n  chartPhase: ChartPhase;\n  chartStatus: ChartStatus;\n  loadingLabel: undefined;\n  yDomainTweenDuration: number;\n  yDomainSkeletonByAxis: Record<string, [number, number]>;\n  yDomainTargetByAxis: Record<string, [number, number]>;\n};\n",
      "type": "registry:component",
      "target": "components/charts/chart-phase.ts"
    },
    {
      "path": "src/charts/y-domain-utils.ts",
      "content": "import { scaleLinear } from \"@visx/scale\";\nimport type { LineConfig } from \"./chart-context\";\nimport { type ChartPhase, Y_DOMAIN_TWEEN_SKIP_THRESHOLD } from \"./chart-phase\";\nimport { groupLinesByYAxisId, normalizeYAxisId } from \"./y-axis-scales\";\n\nexport type YDomain = [number, number];\n\n/** Apply visx `nice()` to raw domain endpoints for stable grid ticks. */\nexport function niceYDomain(domain: YDomain): YDomain {\n  const scale = scaleLinear({ domain, range: [0, 1], nice: true });\n  const niceDomain = scale.domain();\n  return [niceDomain[0] ?? domain[0], niceDomain[1] ?? domain[1]];\n}\n\n/**\n * Skip Y tween when both endpoints move less than the threshold relative to span.\n * When in doubt callers should tween — beauty wins over micro-optimization.\n */\nexport function shouldTweenYDomain(from: YDomain, to: YDomain): boolean {\n  const span = Math.max(\n    Math.abs(to[1] - to[0]),\n    Math.abs(from[1] - from[0]),\n    1\n  );\n  const deltaMin = Math.abs(to[0] - from[0]) / span;\n  const deltaMax = Math.abs(to[1] - from[1]) / span;\n  return (\n    deltaMin >= Y_DOMAIN_TWEEN_SKIP_THRESHOLD ||\n    deltaMax >= Y_DOMAIN_TWEEN_SKIP_THRESHOLD\n  );\n}\n\n/** Phases where the chart shows loading chrome (shimmer, pulse, label). */\nexport function isLoadingChromePhase(phase: ChartPhase): boolean {\n  return phase === \"loading\" || phase === \"revealingLoading\";\n}\n\n/** Phases where grid lines use loading stroke styling (muted / dashed chrome). */\nexport function isLoadingGridChromePhase(phase: ChartPhase): boolean {\n  return (\n    phase === \"loading\" || phase === \"exiting\" || phase === \"gridTweenLoading\"\n  );\n}\n\n/** Phases where Y-domain tween runs after the series has exited. */\nexport function isYDomainTweenPhase(phase: ChartPhase): boolean {\n  return phase === \"gridTweenLoading\" || phase === \"gridTweenReady\";\n}\n\n/** Phases where {@link ReferenceArea} bands are shown (fade in/out on transitions). */\nexport function isReferenceAreaVisiblePhase(phase: ChartPhase): boolean {\n  return (\n    phase === \"ready\" || phase === \"revealing\" || phase === \"gridTweenReady\"\n  );\n}\n\nexport function resolveAnimatedYDestinationDomains(\n  chartPhase: ChartPhase,\n  skeletonByAxis: Record<string, YDomain>,\n  targetByAxis: Record<string, YDomain>\n): Record<string, YDomain> {\n  switch (chartPhase) {\n    case \"loading\":\n    case \"exiting\":\n    case \"gridTweenLoading\":\n      return skeletonByAxis;\n    case \"exitingReady\":\n    case \"gridTweenReady\":\n    case \"revealing\":\n    case \"ready\":\n      return targetByAxis;\n    default:\n      return targetByAxis;\n  }\n}\n\nexport function computeYDomainsByAxis({\n  lines,\n  resolveDomain,\n}: {\n  lines: LineConfig[];\n  resolveDomain: (dataKeys: string[]) => YDomain;\n}): Record<string, YDomain> {\n  const groups = groupLinesByYAxisId(lines);\n  const domains: Record<string, YDomain> = {};\n\n  for (const [axisId, axisLines] of groups) {\n    const dataKeys = axisLines.map((line) => line.dataKey);\n    domains[normalizeYAxisId(axisId)] = niceYDomain(resolveDomain(dataKeys));\n  }\n\n  if (!domains.left) {\n    domains.left = niceYDomain([0, 100]);\n  }\n\n  return domains;\n}\n\n/** Merge domain maps, normalizing axis ids to strings. */\nexport function mergeYDomainRecords(\n  ...records: Record<string, YDomain>[]\n): Record<string, YDomain> {\n  const merged: Record<string, YDomain> = {};\n  for (const record of records) {\n    for (const [axisId, domain] of Object.entries(record)) {\n      merged[normalizeYAxisId(axisId)] = domain;\n    }\n  }\n  return merged;\n}\n\nexport function domainsEqual(\n  left: Record<string, YDomain>,\n  right: Record<string, YDomain>\n): boolean {\n  const leftKeys = Object.keys(left);\n  const rightKeys = Object.keys(right);\n  if (leftKeys.length !== rightKeys.length) {\n    return false;\n  }\n\n  for (const axisId of leftKeys) {\n    const from = left[axisId];\n    const to = right[axisId];\n    if (!(from && to) || from[0] !== to[0] || from[1] !== to[1]) {\n      return false;\n    }\n  }\n\n  return true;\n}\n",
      "type": "registry:lib",
      "target": "components/charts/y-domain-utils.ts"
    },
    {
      "path": "src/charts/filter-data-by-x-domain.ts",
      "content": "export function filterDataByXDomain(\n  data: Record<string, unknown>[],\n  xDomain: [Date, Date],\n  xAccessor: (d: Record<string, unknown>) => Date\n): Record<string, unknown>[] {\n  const start = xDomain[0].getTime();\n  const end = xDomain[1].getTime();\n  const minTime = Math.min(start, end);\n  const maxTime = Math.max(start, end);\n\n  return data.filter((d) => {\n    const time = xAccessor(d).getTime();\n    return time >= minTime && time <= maxTime;\n  });\n}\n\nexport function resolveDataXExtent(\n  data: Record<string, unknown>[],\n  xAccessor: (d: Record<string, unknown>) => Date\n): [Date, Date] | null {\n  if (data.length === 0) {\n    return null;\n  }\n\n  let minTime = Number.POSITIVE_INFINITY;\n  let maxTime = Number.NEGATIVE_INFINITY;\n\n  for (const point of data) {\n    const time = xAccessor(point).getTime();\n    if (time < minTime) {\n      minTime = time;\n    }\n    if (time > maxTime) {\n      maxTime = time;\n    }\n  }\n\n  if (minTime === Number.POSITIVE_INFINITY) {\n    return null;\n  }\n\n  return [new Date(minTime), new Date(maxTime)];\n}\n\n/** Brush track extent — optionally extends past the last data row (e.g. projections). */\nexport function resolveBrushTrackXExtent(\n  data: Record<string, unknown>[],\n  xAccessor: (d: Record<string, unknown>) => Date,\n  xExtentMax?: Date\n): [Date, Date] | null {\n  const extent = resolveDataXExtent(data, xAccessor);\n  if (!extent) {\n    return null;\n  }\n  if (!xExtentMax || xExtentMax.getTime() <= extent[1].getTime()) {\n    return extent;\n  }\n  return [extent[0], xExtentMax];\n}\n",
      "type": "registry:lib",
      "target": "components/charts/filter-data-by-x-domain.ts"
    },
    {
      "path": "src/charts/generate-chart-skeleton-data.ts",
      "content": "const DEFAULT_SKELETON_DATA_KEY = \"value\";\nconst DEFAULT_SKELETON_POINT_COUNT = 7;\n\nexport interface GenerateChartSkeletonDataOptions {\n  /** Key used for y values in each row. Default: `\"value\"`. */\n  dataKey?: string;\n  /** Number of points. Default: 7. */\n  pointCount?: number;\n  /** Start date for the x axis. Default: 2025-01-01. */\n  baseDate?: Date;\n}\n\n/** Placeholder series used while `status=\"loading\"` and data is empty. */\nexport function generateChartSkeletonData(\n  options: GenerateChartSkeletonDataOptions = {}\n): Record<string, unknown>[] {\n  const dataKey = options.dataKey ?? DEFAULT_SKELETON_DATA_KEY;\n  const pointCount = options.pointCount ?? DEFAULT_SKELETON_POINT_COUNT;\n  const baseDate = options.baseDate ?? new Date(\"2025-01-01\");\n\n  return Array.from({ length: pointCount }, (_, index) => {\n    const date = new Date(baseDate);\n    date.setDate(baseDate.getDate() + index);\n    return {\n      date,\n      [dataKey]: Math.round(110 + Math.sin(index * 1.15) * 36 + index * 9),\n    };\n  });\n}\n\n/** Skeleton rows that mirror target dates/count with lower magnitudes for Y tween. */\nexport function generateChartSkeletonFromTarget(\n  targetData: Record<string, unknown>[],\n  dataKey: string\n): Record<string, unknown>[] {\n  return targetData.map((row, index) => ({\n    ...row,\n    [dataKey]: Math.round(95 + Math.sin(index * 1.05) * 28 + index * 7),\n  }));\n}\n\nexport { DEFAULT_SKELETON_DATA_KEY, DEFAULT_SKELETON_POINT_COUNT };\n",
      "type": "registry:lib",
      "target": "components/charts/generate-chart-skeleton-data.ts"
    },
    {
      "path": "src/charts/use-animated-y-domains.ts",
      "content": "\"use client\";\n\nimport { animate, useReducedMotion } from \"motion/react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport type { ChartPhase } from \"./chart-phase\";\nimport { LINE_LOADING_PULSE_EASE } from \"./line-loading-timing\";\nimport {\n  domainsEqual,\n  isYDomainTweenPhase,\n  resolveAnimatedYDestinationDomains,\n  shouldTweenYDomain,\n  type YDomain,\n} from \"./y-domain-utils\";\n\nfunction lerpDomain(from: YDomain, to: YDomain, progress: number): YDomain {\n  return [\n    from[0] + (to[0] - from[0]) * progress,\n    from[1] + (to[1] - from[1]) * progress,\n  ];\n}\n\nfunction snapDomains(\n  domains: Record<string, YDomain>,\n  setAnimatedByAxis: (domains: Record<string, YDomain>) => void,\n  animatedRef: { current: Record<string, YDomain> }\n) {\n  if (domainsEqual(animatedRef.current, domains)) {\n    return;\n  }\n  setAnimatedByAxis(domains);\n  animatedRef.current = domains;\n}\n\nfunction tweenDomains({\n  destination,\n  durationMs,\n  enabled,\n  reducedMotion,\n  animatedRef,\n  setAnimatedByAxis,\n  onSettled,\n}: {\n  destination: Record<string, YDomain>;\n  durationMs: number;\n  enabled: boolean;\n  reducedMotion: boolean | null;\n  animatedRef: { current: Record<string, YDomain> };\n  setAnimatedByAxis: (domains: Record<string, YDomain>) => void;\n  onSettled?: () => void;\n}) {\n  if (domainsEqual(animatedRef.current, destination)) {\n    onSettled?.();\n    return;\n  }\n\n  if (!enabled || reducedMotion) {\n    snapDomains(destination, setAnimatedByAxis, animatedRef);\n    onSettled?.();\n    return;\n  }\n\n  const axisIds = Object.keys(destination);\n  const fromSnapshot = animatedRef.current;\n\n  let needsTween = false;\n  for (const axisId of axisIds) {\n    const from =\n      fromSnapshot[axisId] ?? destination[axisId] ?? ([0, 100] as YDomain);\n    const to = destination[axisId] ?? from;\n    if (shouldTweenYDomain(from, to)) {\n      needsTween = true;\n      break;\n    }\n  }\n\n  if (!needsTween) {\n    snapDomains(destination, setAnimatedByAxis, animatedRef);\n    onSettled?.();\n    return;\n  }\n\n  const fromByAxis: Record<string, YDomain> = {};\n  for (const axisId of axisIds) {\n    fromByAxis[axisId] = fromSnapshot[axisId] ??\n      destination[axisId] ?? [0, 100];\n  }\n\n  const control = animate(0, 1, {\n    duration: durationMs / 1000,\n    ease: [...LINE_LOADING_PULSE_EASE],\n    onUpdate: (progress) => {\n      const next: Record<string, YDomain> = {};\n      for (const axisId of axisIds) {\n        const from =\n          fromByAxis[axisId] ?? destination[axisId] ?? ([0, 100] as YDomain);\n        const to = destination[axisId] ?? from;\n        next[axisId] = shouldTweenYDomain(from, to)\n          ? lerpDomain(from, to, progress)\n          : to;\n      }\n      animatedRef.current = next;\n      setAnimatedByAxis(next);\n    },\n    onComplete: () => {\n      snapDomains(destination, setAnimatedByAxis, animatedRef);\n      onSettled?.();\n    },\n  });\n\n  return control;\n}\n\nexport interface UseAnimatedYDomainsOptions {\n  enabled: boolean;\n  durationMs: number;\n  chartPhase: ChartPhase;\n  skeletonByAxis: Record<string, YDomain>;\n  targetByAxis: Record<string, YDomain>;\n  onSettled?: () => void;\n  /** When true, tweens y-domains on target changes while the chart is in the ready phase (e.g. brush zoom). */\n  tweenOnTargetChange?: boolean;\n}\n\nexport function useAnimatedYDomains({\n  enabled,\n  durationMs,\n  chartPhase,\n  skeletonByAxis,\n  targetByAxis,\n  onSettled,\n  tweenOnTargetChange = false,\n}: UseAnimatedYDomainsOptions): Record<string, YDomain> {\n  const reducedMotion = useReducedMotion();\n  const destinationByAxis = resolveAnimatedYDestinationDomains(\n    chartPhase,\n    skeletonByAxis,\n    targetByAxis\n  );\n  const destinationRef = useRef(destinationByAxis);\n  destinationRef.current = destinationByAxis;\n  const skeletonRef = useRef(skeletonByAxis);\n  skeletonRef.current = skeletonByAxis;\n  const targetRef = useRef(targetByAxis);\n  targetRef.current = targetByAxis;\n\n  const [animatedByAxis, setAnimatedByAxis] = useState(destinationByAxis);\n  const animatedRef = useRef(animatedByAxis);\n  const prevPhaseRef = useRef(chartPhase);\n  const onSettledRef = useRef(onSettled);\n  onSettledRef.current = onSettled;\n\n  useEffect(() => {\n    animatedRef.current = animatedByAxis;\n  }, [animatedByAxis]);\n\n  useEffect(() => {\n    if (prevPhaseRef.current === chartPhase) {\n      return;\n    }\n    prevPhaseRef.current = chartPhase;\n\n    const settle = () => {\n      onSettledRef.current?.();\n    };\n\n    // Keep grid spacing frozen while the series exits the viewport.\n    if (chartPhase === \"exiting\") {\n      snapDomains(skeletonRef.current, setAnimatedByAxis, animatedRef);\n      return;\n    }\n    if (chartPhase === \"exitingReady\") {\n      snapDomains(targetRef.current, setAnimatedByAxis, animatedRef);\n      return;\n    }\n    if (chartPhase === \"loading\") {\n      snapDomains(skeletonRef.current, setAnimatedByAxis, animatedRef);\n      return;\n    }\n    if (chartPhase === \"revealing\" || chartPhase === \"ready\") {\n      snapDomains(targetRef.current, setAnimatedByAxis, animatedRef);\n      return;\n    }\n\n    if (!isYDomainTweenPhase(chartPhase)) {\n      return;\n    }\n\n    const control = tweenDomains({\n      destination: destinationRef.current,\n      durationMs,\n      enabled,\n      reducedMotion,\n      animatedRef,\n      setAnimatedByAxis,\n      onSettled: settle,\n    });\n\n    return () => control?.stop();\n  }, [chartPhase, durationMs, enabled, reducedMotion]);\n\n  const targetSignature = JSON.stringify(targetByAxis);\n  const prevTargetSignatureRef = useRef(targetSignature);\n\n  useEffect(() => {\n    const inLivePhase = chartPhase === \"ready\" || chartPhase === \"revealing\";\n\n    if (!inLivePhase) {\n      prevTargetSignatureRef.current = targetSignature;\n      return;\n    }\n\n    if (prevTargetSignatureRef.current === targetSignature) {\n      return;\n    }\n    prevTargetSignatureRef.current = targetSignature;\n\n    if (tweenOnTargetChange && chartPhase === \"ready\") {\n      const control = tweenDomains({\n        destination: targetRef.current,\n        durationMs,\n        enabled,\n        reducedMotion,\n        animatedRef,\n        setAnimatedByAxis,\n        onSettled: () => onSettledRef.current?.(),\n      });\n\n      return () => control?.stop();\n    }\n\n    snapDomains(targetRef.current, setAnimatedByAxis, animatedRef);\n  }, [\n    chartPhase,\n    durationMs,\n    enabled,\n    reducedMotion,\n    targetSignature,\n    tweenOnTargetChange,\n  ]);\n\n  return animatedByAxis;\n}\n",
      "type": "registry:component",
      "target": "components/charts/use-animated-y-domains.ts"
    },
    {
      "path": "src/charts/use-chart-phase-orchestrator.ts",
      "content": "\"use client\";\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport {\n  type ChartPhase,\n  type ChartStatus,\n  resolveRestingChartPhase,\n} from \"./chart-phase\";\n\nexport interface UseChartPhaseOrchestratorOptions {\n  chartStatus: ChartStatus;\n  targetData: Record<string, unknown>[];\n  skeletonData: Record<string, unknown>[];\n  animationDuration: number;\n  yDomainTweenDuration: number;\n  /** Signature of motion URL state — replays clip reveal in Studio. */\n  revealSignature?: string;\n  /** Skip mount/signature enter reveal (static docs previews). */\n  skipEnterReveal?: boolean;\n}\n\nexport function useChartPhaseOrchestrator({\n  chartStatus,\n  targetData,\n  skeletonData,\n  animationDuration,\n  yDomainTweenDuration,\n  revealSignature = \"\",\n  skipEnterReveal = false,\n}: UseChartPhaseOrchestratorOptions) {\n  const [chartPhase, setChartPhase] = useState<ChartPhase>(() =>\n    resolveRestingChartPhase(chartStatus)\n  );\n  const [plotData, setPlotData] = useState<Record<string, unknown>[]>(() =>\n    chartStatus === \"loading\" ? skeletonData : targetData\n  );\n  const [revealEpoch, setRevealEpoch] = useState(0);\n  const [concealEpoch, setConcealEpoch] = useState(0);\n  const [isLoaded, setIsLoaded] = useState(() => chartStatus === \"ready\");\n  const prevStatusRef = useRef(chartStatus);\n  const phaseRef = useRef(chartPhase);\n  phaseRef.current = chartPhase;\n\n  // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: status transition branches for animation durations\n  useEffect(() => {\n    const prevStatus = prevStatusRef.current;\n    if (prevStatus === chartStatus) {\n      return;\n    }\n    prevStatusRef.current = chartStatus;\n\n    if (chartStatus === \"ready\" && prevStatus === \"loading\") {\n      setIsLoaded(false);\n      if (animationDuration <= 0) {\n        if (yDomainTweenDuration <= 0) {\n          setPlotData(targetData);\n          setChartPhase(\"revealing\");\n        } else {\n          setChartPhase(\"gridTweenReady\");\n        }\n      } else {\n        setChartPhase(\"exiting\");\n      }\n      return;\n    }\n\n    if (chartStatus === \"loading\" && prevStatus === \"ready\") {\n      setIsLoaded(false);\n      if (animationDuration <= 0) {\n        if (yDomainTweenDuration <= 0) {\n          setPlotData(skeletonData);\n          setChartPhase(\"loading\");\n        } else {\n          setChartPhase(\"gridTweenLoading\");\n        }\n      } else {\n        setConcealEpoch((epoch) => epoch + 1);\n        setChartPhase(\"exitingReady\");\n      }\n    }\n  }, [\n    animationDuration,\n    chartStatus,\n    skeletonData,\n    targetData,\n    yDomainTweenDuration,\n  ]);\n\n  // biome-ignore lint/correctness/useExhaustiveDependencies: revealSignature replays enter\n  useEffect(() => {\n    if (skipEnterReveal) {\n      return;\n    }\n    if (chartStatus !== \"ready\") {\n      return;\n    }\n    if (phaseRef.current !== \"ready\") {\n      return;\n    }\n\n    setChartPhase(\"revealing\");\n    setIsLoaded(false);\n  }, [animationDuration, chartStatus, revealSignature, skipEnterReveal]);\n\n  useEffect(() => {\n    switch (chartPhase) {\n      case \"loading\":\n        if (chartStatus === \"loading\") {\n          setPlotData(skeletonData);\n        }\n        break;\n      case \"exiting\":\n        setPlotData(skeletonData);\n        break;\n      case \"exitingReady\":\n      case \"gridTweenLoading\":\n      case \"gridTweenReady\":\n      case \"revealing\":\n      case \"ready\":\n        setPlotData(targetData);\n        break;\n      default:\n        break;\n    }\n  }, [chartPhase, chartStatus, skeletonData, targetData]);\n\n  /** Loading pulse exit finished — tween grid to ready spacing next. */\n  const notifyLoadingPulseComplete = useCallback(() => {\n    if (phaseRef.current !== \"exiting\") {\n      return;\n    }\n    setChartPhase(\"gridTweenReady\");\n  }, []);\n\n  /** Ready series conceal finished — tween grid to loading spacing next. */\n  const notifyRevealConcealComplete = useCallback(() => {\n    if (phaseRef.current !== \"exitingReady\") {\n      return;\n    }\n    setChartPhase(\"gridTweenLoading\");\n  }, []);\n\n  /** Grid tween finished — enter the next resting phase. */\n  const notifyYDomainTweenComplete = useCallback(() => {\n    if (phaseRef.current === \"gridTweenLoading\") {\n      setChartPhase(\"loading\");\n      return;\n    }\n    if (phaseRef.current === \"gridTweenReady\") {\n      setChartPhase(\"revealing\");\n    }\n  }, []);\n\n  useEffect(() => {\n    if (chartPhase !== \"revealing\") {\n      return;\n    }\n\n    setRevealEpoch((epoch) => epoch + 1);\n    if (animationDuration <= 0) {\n      setChartPhase(\"ready\");\n      setIsLoaded(true);\n      return;\n    }\n\n    const timer = window.setTimeout(() => {\n      setChartPhase(\"ready\");\n      setIsLoaded(true);\n    }, animationDuration);\n    return () => window.clearTimeout(timer);\n  }, [animationDuration, chartPhase]);\n\n  return {\n    chartPhase,\n    plotData,\n    revealEpoch,\n    concealEpoch,\n    isLoaded,\n    notifyLoadingPulseComplete,\n    notifyRevealConcealComplete,\n    notifyYDomainTweenComplete,\n  };\n}\n",
      "type": "registry:component",
      "target": "components/charts/use-chart-phase-orchestrator.ts"
    },
    {
      "path": "src/charts/line-loading-timing.ts",
      "content": "/** Grow + exit timeline for `LineLoadingPulse` (seconds). */\nexport const LINE_LOADING_PULSE_CYCLE_S = 2.2;\n\n/** Idle gap before the loading line pulse restarts (milliseconds). */\nexport const LINE_LOADING_LOOP_PAUSE_MS = 280;\n\n/** Loading label exit on loading → ready (seconds). */\nexport const LOADING_LABEL_EXIT_S = 0.45;\n\n/** Loading label drops this many pixels while exiting. */\nexport const LOADING_LABEL_EXIT_Y_PX = 30;\n\nexport const LINE_LOADING_PULSE_EASE = [0.85, 0, 0.15, 1] as const;\n",
      "type": "registry:lib",
      "target": "components/charts/line-loading-timing.ts"
    }
  ],
  "cssVars": {
    "light": {
      "--chart-1": "oklch(0.32 0 none)",
      "--chart-2": "oklch(0.41 0 none)",
      "--chart-3": "oklch(0.54 0 none)",
      "--chart-4": "oklch(0.71 0 none)",
      "--chart-5": "oklch(0.89 0 none)",
      "--chart-background": "oklch(1 0 0)",
      "--chart-foreground": "oklch(0.145 0.004 285)",
      "--chart-foreground-muted": "oklch(0.55 0.014 260)",
      "--chart-line-primary": "var(--chart-1)",
      "--chart-line-secondary": "var(--chart-2)",
      "--chart-crosshair": "oklch(0.4 0.1828 274.34)",
      "--chart-grid": "oklch(0.9 0 0)",
      "--chart-brush-border": "var(--chart-grid)",
      "--chart-tooltip-background": "oklch(0.21 0.006 285 / 0.8)",
      "--chart-tooltip-foreground": "oklch(0.985 0 0)",
      "--chart-tooltip-muted": "oklch(0.65 0.01 260)",
      "--chart-marker-background": "oklch(0.97 0.005 260)",
      "--chart-marker-border": "oklch(0.85 0.01 260)",
      "--chart-marker-foreground": "oklch(0.3 0.01 260)",
      "--chart-label": "oklch(0.45 0.01 260)",
      "--chart-scale-01": "oklch(0.98 0.003 106)",
      "--chart-scale-02": "oklch(0.92 0.008 106)",
      "--chart-scale-03": "oklch(0.82 0.015 106)",
      "--chart-scale-04": "oklch(0.68 0.02 106)",
      "--chart-scale-05": "oklch(0.55 0.025 106)",
      "--chart-scale-pattern-color": "oklch(0.96 0.005 106)"
    },
    "dark": {
      "--chart-1": "oklch(1 0 none)",
      "--chart-2": "oklch(0.73 0 none)",
      "--chart-3": "oklch(0.51 0 none)",
      "--chart-4": "oklch(0.39 0 none)",
      "--chart-5": "oklch(0.32 0 none)",
      "--chart-background": "oklch(0.145 0 0)",
      "--chart-foreground": "oklch(0.45 0 0)",
      "--chart-foreground-muted": "oklch(0.65 0.01 260)",
      "--chart-crosshair": "oklch(0.45 0 0)",
      "--chart-grid": "oklch(0.25 0 0)",
      "--chart-brush-border": "var(--chart-grid)",
      "--chart-marker-background": "oklch(0.25 0.01 260)",
      "--chart-marker-border": "oklch(0.4 0.01 260)",
      "--chart-marker-foreground": "oklch(0.9 0 0)",
      "--chart-label": "oklch(0.75 0.01 260)",
      "--chart-scale-01": "oklch(0.28 0.03 270)",
      "--chart-scale-02": "oklch(0.38 0.04 270)",
      "--chart-scale-03": "oklch(0.48 0.05 270)",
      "--chart-scale-04": "oklch(0.58 0.06 270)",
      "--chart-scale-05": "oklch(0.68 0.07 270)",
      "--chart-scale-pattern-color": "oklch(0.145 0 0)"
    }
  }
}