{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "composed-chart",
  "type": "registry:component",
  "title": "Composed Chart",
  "description": "Time-series chart composing Line, Area, and SeriesBar on one shared scale (Recharts ComposedChart-style)",
  "dependencies": [
    "@visx/curve@4.0.1-alpha.0",
    "@visx/scale@4.0.1-alpha.0",
    "@visx/shape@4.0.1-alpha.0",
    "@visx/responsive@4.0.1-alpha.0",
    "d3-array",
    "motion"
  ],
  "registryDependencies": [
    "@bklit/chart-context",
    "@bklit/grid",
    "@bklit/x-axis",
    "@bklit/y-axis",
    "@bklit/chart-tooltip",
    "@bklit/utils",
    "@bklit/line-chart",
    "@bklit/area-chart"
  ],
  "files": [
    {
      "path": "src/charts/composed-chart.tsx",
      "content": "\"use client\";\n\nimport { ParentSize } from \"@visx/responsive\";\nimport type { Transition } from \"motion/react\";\nimport {\n  Children,\n  isValidElement,\n  type ReactElement,\n  type ReactNode,\n  useMemo,\n  useRef,\n} from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { Area, type AreaProps } from \"./area\";\nimport type { LineConfig, Margin } from \"./chart-context\";\nimport type { ChartPhase } from \"./chart-phase\";\nimport { Line, type LineProps } from \"./line\";\nimport { SeriesBar, type SeriesBarProps } from \"./series-bar\";\nimport { TimeSeriesChartInner } from \"./time-series-chart-shell\";\n\nexport interface ComposedChartProps {\n  /** Data array — each row typically has a date and multiple numeric series */\n  data: Record<string, unknown>[];\n  /** Key for the x-axis (time). Default: \"date\" */\n  xDataKey?: string;\n  margin?: Partial<Margin>;\n  animationDuration?: number;\n  animationEasing?: string;\n  enterTransition?: Transition;\n  /** Signature of motion URL state — triggers reveal replay when it changes. */\n  revealSignature?: string;\n  aspectRatio?: string;\n  className?: string;\n  children: ReactNode;\n  /** Target bar width in px (Recharts-style `barSize`). */\n  barSize?: number;\n  /** Maximum bar width in px (`maxBarSize`). */\n  maxBarSize?: number;\n  /** Gap between grouped `SeriesBar` series in px. Default: 4 */\n  barGap?: number;\n  /** Stack `SeriesBar` segments in child order at each x (line/area are not stacked). */\n  stacked?: boolean;\n  /** Gap in px between stacked segments. Default: 0 */\n  stackGap?: number;\n  onPhaseChange?: (phase: ChartPhase) => void;\n}\n\nconst DEFAULT_MARGIN: Margin = { top: 40, right: 40, bottom: 40, left: 40 };\n\nfunction getChildComponentName(child: ReactElement): string {\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 upsertLineConfig(lines: LineConfig[], config: LineConfig): void {\n  const index = lines.findIndex((line) => line.dataKey === config.dataKey);\n  if (index === -1) {\n    lines.push(config);\n    return;\n  }\n  // Area+Line pairs share a dataKey — keep the later config (Line over Area).\n  lines[index] = config;\n}\n\nfunction tryAppendSeriesBar(\n  child: ReactElement,\n  lines: LineConfig[],\n  barDataKeys: string[]\n): boolean {\n  const name = getChildComponentName(child);\n  if (!(child.type === SeriesBar || name === \"SeriesBar\")) {\n    return false;\n  }\n  const props = child.props as SeriesBarProps;\n  if (!props.dataKey) {\n    return true;\n  }\n  barDataKeys.push(props.dataKey);\n  upsertLineConfig(lines, {\n    dataKey: props.dataKey,\n    stroke: props.stroke || props.fill || \"var(--chart-line-primary)\",\n    strokeWidth: 0,\n  });\n  return true;\n}\n\nfunction tryAppendLine(child: ReactElement, lines: LineConfig[]): boolean {\n  const name = getChildComponentName(child);\n  if (!(child.type === Line || name === \"Line\")) {\n    return false;\n  }\n  const props = child.props as LineProps;\n  if (props.dataKey) {\n    upsertLineConfig(lines, {\n      dataKey: props.dataKey,\n      stroke: props.stroke || \"var(--chart-line-primary)\",\n      strokeWidth: props.strokeWidth ?? 2.5,\n      yAxisId: props.yAxisId,\n    });\n  }\n  return true;\n}\n\nfunction tryAppendArea(child: ReactElement, lines: LineConfig[]): boolean {\n  const name = getChildComponentName(child);\n  if (!(child.type === Area || name === \"Area\")) {\n    return false;\n  }\n  const props = child.props as AreaProps;\n  if (props.dataKey) {\n    upsertLineConfig(lines, {\n      dataKey: props.dataKey,\n      stroke: props.stroke || props.fill || \"var(--chart-line-primary)\",\n      strokeWidth: props.strokeWidth ?? 2,\n      yAxisId: props.yAxisId,\n    });\n  }\n  return true;\n}\n\nfunction extractComposedSeries(children: ReactNode): {\n  lines: LineConfig[];\n  barDataKeys: string[];\n} {\n  const lines: LineConfig[] = [];\n  const barDataKeys: string[] = [];\n\n  Children.forEach(children, (child) => {\n    if (!isValidElement(child)) {\n      return;\n    }\n    if (tryAppendSeriesBar(child, lines, barDataKeys)) {\n      return;\n    }\n    if (tryAppendLine(child, lines)) {\n      return;\n    }\n    tryAppendArea(child, lines);\n  });\n\n  return { lines, barDataKeys };\n}\n\nfunction computeComposedYScaleDomainMax(\n  data: Record<string, unknown>[],\n  lines: LineConfig[],\n  barDataKeys: string[]\n): number | undefined {\n  const barSet = new Set(barDataKeys);\n  let max = 0;\n  for (const d of data) {\n    let barSum = 0;\n    for (const k of barDataKeys) {\n      const v = d[k];\n      if (typeof v === \"number\") {\n        barSum += v;\n      }\n    }\n    let rowMaxOther = 0;\n    for (const line of lines) {\n      if (barSet.has(line.dataKey)) {\n        continue;\n      }\n      const v = d[line.dataKey];\n      if (typeof v === \"number\") {\n        rowMaxOther = Math.max(rowMaxOther, v);\n      }\n    }\n    max = Math.max(max, barSum, rowMaxOther);\n  }\n  return max > 0 ? max : undefined;\n}\n\ninterface ChartInnerProps {\n  width: number;\n  height: number;\n  data: Record<string, unknown>[];\n  xDataKey: string;\n  margin: Margin;\n  animationDuration: number;\n  animationEasing?: string;\n  enterTransition?: Transition;\n  revealSignature?: string;\n  children: ReactNode;\n  containerRef: React.RefObject<HTMLDivElement | null>;\n  barSize?: number;\n  maxBarSize?: number;\n  barGap?: number;\n  stacked?: boolean;\n  stackGap?: number;\n  onPhaseChange?: (phase: ChartPhase) => void;\n}\n\nfunction ChartInner({\n  width,\n  height,\n  data,\n  xDataKey,\n  margin,\n  animationDuration,\n  animationEasing,\n  enterTransition,\n  revealSignature,\n  children,\n  containerRef,\n  barSize,\n  maxBarSize,\n  barGap,\n  stacked = false,\n  stackGap = 0,\n  onPhaseChange,\n}: ChartInnerProps) {\n  const { lines, barDataKeys } = useMemo(\n    () => extractComposedSeries(children),\n    [children]\n  );\n\n  const composedStackOffsets = useMemo(() => {\n    if (!(stacked && barDataKeys.length > 0)) {\n      return undefined;\n    }\n    const offsets = new Map<number, Map<string, number>>();\n    for (let i = 0; i < data.length; i++) {\n      const d = data[i];\n      if (!d) {\n        continue;\n      }\n      const pointOffsets = new Map<string, number>();\n      let cumulative = 0;\n      for (const key of barDataKeys) {\n        pointOffsets.set(key, cumulative);\n        const v = d[key];\n        if (typeof v === \"number\") {\n          cumulative += v;\n        }\n      }\n      offsets.set(i, pointOffsets);\n    }\n    return offsets;\n  }, [data, barDataKeys, stacked]);\n\n  const yScaleDomainMax = useMemo(\n    () =>\n      stacked && barDataKeys.length > 0\n        ? computeComposedYScaleDomainMax(data, lines, barDataKeys)\n        : undefined,\n    [data, lines, barDataKeys, stacked]\n  );\n\n  return (\n    <TimeSeriesChartInner\n      animationDuration={animationDuration}\n      animationEasing={animationEasing}\n      clipPathId=\"composed-chart-grow-clip\"\n      composedBarDataKeys={barDataKeys.length > 0 ? barDataKeys : undefined}\n      composedBarGap={barGap}\n      composedBarSize={barSize}\n      composedMaxBarSize={maxBarSize}\n      composedStacked={stacked}\n      composedStackGap={stackGap}\n      composedStackOffsets={composedStackOffsets}\n      containerRef={containerRef}\n      data={data}\n      enterTransition={enterTransition}\n      height={height}\n      lines={lines}\n      margin={margin}\n      onPhaseChange={onPhaseChange}\n      revealSignature={revealSignature}\n      width={width}\n      xDataKey={xDataKey}\n      yScaleDomainMax={yScaleDomainMax}\n    >\n      {children}\n    </TimeSeriesChartInner>\n  );\n}\n\nexport function ComposedChart({\n  data,\n  xDataKey = \"date\",\n  margin: marginProp,\n  animationDuration = 1100,\n  animationEasing,\n  enterTransition,\n  revealSignature,\n  aspectRatio = \"2 / 1\",\n  className = \"\",\n  children,\n  barSize,\n  maxBarSize,\n  barGap = 4,\n  stacked = false,\n  stackGap = 0,\n  onPhaseChange,\n}: ComposedChartProps) {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const margin = { ...DEFAULT_MARGIN, ...marginProp };\n\n  return (\n    <div\n      className={cn(\"relative w-full\", className)}\n      ref={containerRef}\n      style={{ aspectRatio, touchAction: \"none\" }}\n    >\n      <ParentSize debounceTime={10}>\n        {({ width, height }) => (\n          <ChartInner\n            animationDuration={animationDuration}\n            animationEasing={animationEasing}\n            barGap={barGap}\n            barSize={barSize}\n            containerRef={containerRef}\n            data={data}\n            enterTransition={enterTransition}\n            height={height}\n            margin={margin}\n            maxBarSize={maxBarSize}\n            onPhaseChange={onPhaseChange}\n            revealSignature={revealSignature}\n            stacked={stacked}\n            stackGap={stackGap}\n            width={width}\n            xDataKey={xDataKey}\n          >\n            {children}\n          </ChartInner>\n        )}\n      </ParentSize>\n    </div>\n  );\n}\n\nComposedChart.displayName = \"ComposedChart\";\n\nexport default ComposedChart;\n",
      "type": "registry:component",
      "target": "components/charts/composed-chart.tsx"
    },
    {
      "path": "src/charts/series-bar.tsx",
      "content": "\"use client\";\n\nimport type { Transition } from \"motion/react\";\nimport { motion } from \"motion/react\";\nimport { useMemo } from \"react\";\nimport { chartCssVars, useChart } from \"./chart-context\";\nimport { useChartLegendHover } from \"./chart-legend-hover\";\nimport { transitionWithDelay } from \"./motion-utils\";\nimport { computeSeriesBarWidth } from \"./series-bar-layout\";\n\nfunction computeSeriesBarLayout(input: {\n  stacked: boolean;\n  composedStackOffsets: Map<number, Map<string, number>> | undefined;\n  rowIndex: number;\n  dataKey: string;\n  value: number;\n  yScale: (n: number) => number | undefined;\n  innerHeight: number;\n  xCenter: number;\n  barWidth: number;\n  seriesCount: number;\n  gap: number;\n  seriesIndex: number;\n  stackGap: number;\n  isLastSeries: boolean;\n  radius: number;\n}): {\n  barLeft: number;\n  barHeight: number;\n  effectiveRadius: number;\n  valueY: number;\n} {\n  const {\n    stacked,\n    composedStackOffsets,\n    rowIndex,\n    dataKey,\n    value,\n    yScale,\n    innerHeight,\n    xCenter,\n    barWidth,\n    seriesCount,\n    gap,\n    seriesIndex,\n    stackGap,\n    isLastSeries,\n    radius,\n  } = input;\n\n  if (stacked && composedStackOffsets) {\n    const offset = composedStackOffsets.get(rowIndex)?.get(dataKey) ?? 0;\n    const valuePos = yScale(value) ?? 0;\n    let barHeight = innerHeight - valuePos;\n    const offsetY = yScale(offset) ?? innerHeight;\n    const gapOffset = seriesIndex * stackGap;\n    const valueY = offsetY - barHeight - gapOffset;\n    if (!isLastSeries && stackGap > 0) {\n      barHeight = Math.max(0, barHeight - stackGap);\n    }\n    const barLeft = xCenter - barWidth / 2;\n    const applyRounding = stackGap > 0 || isLastSeries;\n    return {\n      barLeft,\n      barHeight,\n      effectiveRadius: applyRounding ? radius : 0,\n      valueY,\n    };\n  }\n\n  const groupWidth =\n    seriesCount * barWidth + (seriesCount > 1 ? (seriesCount - 1) * gap : 0);\n  const valueY = yScale(value) ?? innerHeight;\n  return {\n    barLeft: xCenter - groupWidth / 2 + seriesIndex * (barWidth + gap),\n    barHeight: innerHeight - valueY,\n    effectiveRadius: radius,\n    valueY,\n  };\n}\n\nexport interface SeriesBarProps {\n  /** Key in data for bar height (y value) */\n  dataKey: string;\n  /** Fill color. Default: var(--chart-line-primary) */\n  fill?: string;\n  /** Tooltip dot color when fill is gradient/pattern. Default: fill */\n  stroke?: string;\n  /** Corner radius for bar top corners. Default: 0 (square tops, similar to Bar lineCap=\"butt\") */\n  radius?: number;\n  /** Animate grow from baseline. Default: true */\n  animate?: boolean;\n  /** Opacity for non-hovered bars when another point is hovered (matches BarChart). Default: 0.3 */\n  fadedOpacity?: number;\n}\n\nexport function SeriesBar({\n  dataKey,\n  fill = chartCssVars.linePrimary,\n  radius = 0,\n  animate = true,\n  fadedOpacity = 0.3,\n}: SeriesBarProps) {\n  const {\n    data,\n    xScale,\n    yScale,\n    xAccessor,\n    innerHeight,\n    innerWidth,\n    columnWidth,\n    isLoaded,\n    animationDuration,\n    enterTransition,\n    revealEpoch = 0,\n    barScale,\n    composedBarDataKeys,\n    composedBarSize,\n    composedMaxBarSize,\n    composedBarGap,\n    composedStacked,\n    composedStackOffsets,\n    composedStackGap,\n    tooltipData,\n  } = useChart();\n\n  const barKeys = useMemo(() => {\n    if (composedBarDataKeys && composedBarDataKeys.length > 0) {\n      return composedBarDataKeys;\n    }\n    return [dataKey];\n  }, [composedBarDataKeys, dataKey]);\n\n  const seriesIndex = useMemo(() => {\n    const idx = barKeys.indexOf(dataKey);\n    return idx >= 0 ? idx : 0;\n  }, [barKeys, dataKey]);\n\n  const n = barKeys.length;\n  const gap = composedBarGap ?? 4;\n  const stackGap = composedStackGap ?? 0;\n\n  const stacked =\n    Boolean(composedStacked) &&\n    composedStackOffsets != null &&\n    composedBarDataKeys != null &&\n    composedBarDataKeys.length > 0;\n\n  const isLastSeries = seriesIndex === n - 1;\n\n  const barWidth = useMemo(\n    () =>\n      computeSeriesBarWidth({\n        innerWidth,\n        dataLength: data.length,\n        columnWidth,\n        seriesCount: n,\n        composedBarSize,\n        composedMaxBarSize,\n        composedBarGap: gap,\n        stacked,\n      }),\n    [\n      columnWidth,\n      composedBarSize,\n      composedMaxBarSize,\n      data.length,\n      gap,\n      innerWidth,\n      n,\n      stacked,\n    ]\n  );\n\n  const totalAnimDuration = animationDuration || 1100;\n  const staggerSpread = totalAnimDuration * 0.4;\n  const calculatedStaggerDelay =\n    data.length > 1 ? staggerSpread / 1000 / data.length : 0;\n  const { hoveredIndex: legendHoveredIndex } = useChartLegendHover();\n  const isLegendDimmed =\n    legendHoveredIndex !== null && legendHoveredIndex !== seriesIndex;\n  const hoveredIndex = tooltipData?.index ?? null;\n\n  if (barScale) {\n    console.warn(\n      \"SeriesBar is for time-based ComposedChart / LineChart context. Use Bar inside BarChart for categorical x.\"\n    );\n    return null;\n  }\n\n  return (\n    <g className=\"series-bar\">\n      {data.map((d, i) => {\n        const value = d[dataKey];\n        if (typeof value !== \"number\") {\n          return null;\n        }\n\n        const xCenter = xScale(xAccessor(d)) ?? 0;\n\n        const { barLeft, valueY, barHeight, effectiveRadius } =\n          computeSeriesBarLayout({\n            stacked,\n            composedStackOffsets,\n            rowIndex: i,\n            dataKey,\n            value,\n            yScale,\n            innerHeight,\n            xCenter,\n            barWidth,\n            seriesCount: n,\n            gap,\n            seriesIndex,\n            stackGap,\n            isLastSeries,\n            radius,\n          });\n\n        const categoryLabel = String(xAccessor(d).getTime());\n        const isFaded =\n          (hoveredIndex !== null && hoveredIndex !== i) || isLegendDimmed;\n\n        if (animate && !isLoaded) {\n          return (\n            <SeriesBarRect\n              barHeight={barHeight}\n              barWidth={barWidth}\n              calculatedStaggerDelay={calculatedStaggerDelay}\n              enterTransition={enterTransition}\n              fadedOpacity={fadedOpacity}\n              fill={fill}\n              index={i}\n              innerHeight={innerHeight}\n              isFaded={isFaded}\n              key={`${dataKey}-${categoryLabel}-${revealEpoch}`}\n              radius={effectiveRadius}\n              revealEpoch={revealEpoch}\n              x={barLeft}\n              y={valueY}\n            />\n          );\n        }\n\n        return (\n          <motion.rect\n            animate={{ opacity: isFaded ? fadedOpacity : 1 }}\n            fill={fill}\n            height={barHeight}\n            key={`${dataKey}-${categoryLabel}`}\n            rx={effectiveRadius}\n            ry={effectiveRadius}\n            transition={{ opacity: { duration: 0.12 } }}\n            width={barWidth}\n            x={barLeft}\n            y={valueY}\n          />\n        );\n      })}\n    </g>\n  );\n}\n\nSeriesBar.displayName = \"SeriesBar\";\n\ninterface SeriesBarRectProps {\n  x: number;\n  y: number;\n  barWidth: number;\n  barHeight: number;\n  fill: string;\n  radius: number;\n  index: number;\n  innerHeight: number;\n  calculatedStaggerDelay: number;\n  enterTransition?: Transition;\n  revealEpoch: number;\n  isFaded: boolean;\n  fadedOpacity: number;\n}\n\nfunction SeriesBarRect({\n  x,\n  y,\n  barWidth,\n  barHeight,\n  fill,\n  radius,\n  index,\n  innerHeight,\n  calculatedStaggerDelay,\n  enterTransition,\n  revealEpoch,\n  isFaded,\n  fadedOpacity,\n}: SeriesBarRectProps) {\n  const enterAnim = transitionWithDelay(\n    enterTransition,\n    index * calculatedStaggerDelay\n  );\n\n  return (\n    <motion.rect\n      animate={{\n        height: barHeight,\n        y,\n        opacity: isFaded ? fadedOpacity : 1,\n      }}\n      fill={fill}\n      initial={{ height: 0, y: innerHeight, opacity: 1 }}\n      key={`series-bar-${index}-${revealEpoch}`}\n      rx={radius}\n      ry={radius}\n      transition={enterAnim}\n      width={barWidth}\n      x={x}\n    />\n  );\n}\n\nexport default SeriesBar;\n",
      "type": "registry:component",
      "target": "components/charts/series-bar.tsx"
    },
    {
      "path": "src/charts/series-bar-layout.ts",
      "content": "export function computeSeriesBarWidth(input: {\n  innerWidth: number;\n  dataLength: number;\n  columnWidth: number;\n  seriesCount: number;\n  composedBarSize?: number;\n  composedMaxBarSize?: number;\n  composedBarGap?: number;\n  stacked?: boolean;\n}): number {\n  const {\n    innerWidth,\n    dataLength,\n    columnWidth,\n    seriesCount,\n    composedBarSize,\n    composedMaxBarSize,\n    composedBarGap = 4,\n    stacked = false,\n  } = input;\n\n  const gap = composedBarGap;\n  const groupCount = stacked ? 1 : Math.max(1, seriesCount);\n  let slot = columnWidth;\n  if (slot <= 0) {\n    slot = dataLength < 2 ? innerWidth : innerWidth / (dataLength - 1);\n  }\n\n  let width =\n    composedBarSize ??\n    Math.min(slot * 0.88, composedMaxBarSize ?? Number.POSITIVE_INFINITY);\n  if (composedMaxBarSize != null) {\n    width = Math.min(width, composedMaxBarSize);\n  }\n  if (groupCount > 1) {\n    const maxGroup = slot * 0.92;\n    const needed = groupCount * width + (groupCount - 1) * gap;\n    if (needed > maxGroup && maxGroup > 0) {\n      width = Math.max(4, (maxGroup - (groupCount - 1) * gap) / groupCount);\n    }\n  }\n\n  return Math.max(2, width);\n}\n\n/** Half-width of the bar group at each x — used to pad reveal clips. */\nexport function computeSeriesBarRevealClipPadding(input: {\n  barWidth: number;\n  seriesCount: number;\n  gap?: number;\n  stacked?: boolean;\n}): number {\n  const { barWidth, seriesCount, gap = 4, stacked = false } = input;\n\n  if (stacked || seriesCount <= 1) {\n    return Math.ceil(barWidth / 2);\n  }\n\n  const groupWidth = seriesCount * barWidth + (seriesCount - 1) * gap;\n  return Math.ceil(groupWidth / 2);\n}\n",
      "type": "registry:component",
      "target": "components/charts/series-bar-layout.ts"
    },
    {
      "path": "src/charts/time-series-chart-shell.tsx",
      "content": "\"use client\";\n\nimport { scaleLinear, scaleTime } from \"@visx/scale\";\nimport { bisector, extent } from \"d3-array\";\nimport type { Transition } from \"motion/react\";\nimport {\n  Children,\n  cloneElement,\n  isValidElement,\n  memo,\n  type ReactElement,\n  type ReactNode,\n  useCallback,\n  useEffect,\n  useMemo,\n  useState,\n} from \"react\";\nimport {\n  DEFAULT_ANIMATION_EASING,\n  DEFAULT_CHART_ENTER_TRANSITION,\n} from \"./animation\";\nimport {\n  isClipExcludedComponent,\n  isPostOverlayComponent,\n  isUnderlayComponent,\n  resolveChartChildElement,\n} from \"./chart-child-passthrough\";\nimport { ChartProvider, type LineConfig, type Margin } from \"./chart-context\";\nimport { isGradientDefComponent, isPatternDefComponent } from \"./chart-defs\";\nimport { shortDateFmt } from \"./chart-formatters\";\nimport {\n  type ChartPhase,\n  type ChartStatus,\n  DEFAULT_CHART_STATUS,\n  DEFAULT_Y_DOMAIN_TWEEN_MS,\n  isChartInteractionPhase,\n} from \"./chart-phase\";\nimport { ChartRevealClip } from \"./chart-reveal-clip\";\nimport {\n  decimateTimeSeries,\n  maxRenderPointsForWidth,\n} from \"./decimate-time-series\";\nimport { filterDataByXDomain } from \"./filter-data-by-x-domain\";\nimport {\n  generateChartSkeletonData,\n  generateChartSkeletonFromTarget,\n} from \"./generate-chart-skeleton-data\";\nimport {\n  extractProjectionLineConfigs,\n  mergeProjectionXDomainMax,\n  mergeProjectionYDomain,\n} from \"./projection-config\";\nimport {\n  extractReferenceAreaConfigs,\n  type ReferenceAreaConfig,\n} from \"./reference-area-config\";\nimport { ReferenceAreaRegistrationContext } from \"./reference-area-registration-context\";\nimport {\n  computeSeriesBarRevealClipPadding,\n  computeSeriesBarWidth,\n} from \"./series-bar-layout\";\nimport { useStaticChartPreview } from \"./static-chart-preview-context\";\nimport { useAnimatedYDomains } from \"./use-animated-y-domains\";\nimport { useChartInteraction } from \"./use-chart-interaction\";\nimport { useChartPhaseOrchestrator } from \"./use-chart-phase-orchestrator\";\nimport {\n  buildYScalesFromDomains,\n  DEFAULT_Y_AXIS_ID,\n  getPrimaryYScale,\n  groupLinesByYAxisId,\n} from \"./y-axis-scales\";\nimport { computeYDomainsByAxis } from \"./y-domain-utils\";\n\nfunction collectNumericExtents(\n  data: Record<string, unknown>[],\n  dataKeys: string[]\n) {\n  let minValue = Number.POSITIVE_INFINITY;\n  let maxValue = Number.NEGATIVE_INFINITY;\n\n  for (const d of data) {\n    for (const key of dataKeys) {\n      const value = d[key];\n      if (typeof value === \"number\") {\n        if (value < minValue) {\n          minValue = value;\n        }\n        if (value > maxValue) {\n          maxValue = value;\n        }\n      }\n    }\n  }\n\n  if (minValue === Number.POSITIVE_INFINITY) {\n    return { minValue: 0, maxValue: 100 };\n  }\n\n  return { minValue, maxValue };\n}\n\nfunction resolveTimeSeriesYDomain(\n  data: Record<string, unknown>[],\n  dataKeys: string[],\n  yScaleDomainMax: number | undefined\n): [number, number] {\n  if (yScaleDomainMax != null && yScaleDomainMax > 0) {\n    return [0, yScaleDomainMax * 1.1];\n  }\n\n  const { minValue, maxValue } = collectNumericExtents(data, dataKeys);\n\n  if (minValue >= 0) {\n    const top = maxValue <= 0 ? 100 : maxValue * 1.1;\n    return [0, top];\n  }\n\n  const padding = (maxValue - minValue) * 0.05 || 1;\n  return [minValue - padding, maxValue + padding];\n}\n\nfunction ensureChildKey(child: ReactElement, index: number): ReactElement {\n  if (child.key != null) {\n    return child;\n  }\n  return cloneElement(child, { key: `chart-child-${index}` });\n}\n\nexport interface TimeSeriesChartInnerProps {\n  width: number;\n  height: number;\n  data: Record<string, unknown>[];\n  xDataKey: string;\n  margin: Margin;\n  animationDuration: number;\n  animationEasing?: string;\n  enterTransition?: Transition;\n  /** Signature of motion URL state — triggers reveal replay when it changes. */\n  revealSignature?: string;\n  children: ReactNode;\n  containerRef: React.RefObject<HTMLDivElement | null>;\n  /** Series keys driving y-domain and tooltip (Line / Area / SeriesBar configs). */\n  lines: LineConfig[];\n  /** SVG clipPath id for grow animation. */\n  clipPathId: string;\n  /** Optional ComposedChart bar layout (forwarded into context). */\n  composedBarDataKeys?: string[];\n  composedBarSize?: number;\n  composedMaxBarSize?: number;\n  composedBarGap?: number;\n  composedStacked?: boolean;\n  composedStackOffsets?: Map<number, Map<string, number>>;\n  composedStackGap?: number;\n  /** When set, drives the y-axis max instead of scanning `lines` (e.g. stacked bar totals). */\n  yScaleDomainMax?: number;\n  /** Loading vs ready — drives chart phase until transition orchestration lands. */\n  chartStatus?: ChartStatus;\n  loadingLabel?: string;\n  /** Animate y-domain on status / data transitions. Default: true */\n  yDomainTween?: boolean;\n  yDomainTweenDuration?: number;\n  /** Visible x-domain for brush zoom. When set, y-domain and series use data in this range. */\n  xDomain?: [Date, Date];\n  /** Full dataset length for x-scale padding when `xDomain` is set. */\n  xDomainSlotCount?: number;\n  /** Tween y-domain when the visible x-range changes during the ready phase. */\n  tweenYDomainOnXDomainChange?: boolean;\n  onPhaseChange?: (phase: ChartPhase) => void;\n}\n\nexport function TimeSeriesChartInner(props: TimeSeriesChartInnerProps) {\n  const { width, height } = props;\n  if (width < 10 || height < 10) {\n    return null;\n  }\n  return <TimeSeriesChartCore {...props} />;\n}\n\nconst TimeSeriesChartCore = memo(function TimeSeriesChartCore({\n  width,\n  height,\n  data,\n  xDataKey,\n  margin,\n  animationDuration,\n  animationEasing = DEFAULT_ANIMATION_EASING,\n  enterTransition,\n  revealSignature = \"\",\n  children,\n  containerRef,\n  lines,\n  clipPathId,\n  composedBarDataKeys,\n  composedBarSize,\n  composedMaxBarSize,\n  composedBarGap,\n  composedStacked,\n  composedStackOffsets,\n  composedStackGap,\n  yScaleDomainMax,\n  chartStatus = DEFAULT_CHART_STATUS,\n  loadingLabel,\n  yDomainTween = true,\n  yDomainTweenDuration = DEFAULT_Y_DOMAIN_TWEEN_MS,\n  xDomain,\n  xDomainSlotCount,\n  tweenYDomainOnXDomainChange = false,\n  onPhaseChange,\n}: TimeSeriesChartInnerProps) {\n  const staticPreview = useStaticChartPreview();\n  const innerWidth = width - margin.left - margin.right;\n  const innerHeight = height - margin.top - margin.bottom;\n\n  const resolveYDomain = useCallback(\n    (sourceData: Record<string, unknown>[], dataKeys: string[]) => {\n      const axisGroups = groupLinesByYAxisId(lines);\n      const usesDefaultOnly =\n        axisGroups.size === 1 && axisGroups.has(DEFAULT_Y_AXIS_ID);\n      const domainMax =\n        usesDefaultOnly && yScaleDomainMax != null\n          ? yScaleDomainMax\n          : undefined;\n      return resolveTimeSeriesYDomain(sourceData, dataKeys, domainMax);\n    },\n    [lines, yScaleDomainMax]\n  );\n\n  const skeletonData = useMemo(() => {\n    const primaryKey = lines[0]?.dataKey ?? \"value\";\n    if (data.length === 0) {\n      return generateChartSkeletonData({ dataKey: primaryKey });\n    }\n    return generateChartSkeletonFromTarget(data, primaryKey);\n  }, [data, lines]);\n\n  const {\n    chartPhase,\n    plotData,\n    revealEpoch,\n    concealEpoch,\n    isLoaded,\n    notifyLoadingPulseComplete,\n    notifyRevealConcealComplete,\n    notifyYDomainTweenComplete,\n  } = useChartPhaseOrchestrator({\n    animationDuration,\n    chartStatus,\n    revealSignature,\n    skeletonData,\n    skipEnterReveal: staticPreview,\n    targetData: data,\n    yDomainTweenDuration,\n  });\n\n  useEffect(() => {\n    onPhaseChange?.(chartPhase);\n  }, [chartPhase, onPhaseChange]);\n\n  const xAccessor = useCallback(\n    (d: Record<string, unknown>): Date => {\n      const value = d[xDataKey];\n      return value instanceof Date ? value : new Date(value as string | number);\n    },\n    [xDataKey]\n  );\n\n  const bisectDate = useMemo(\n    () => bisector<Record<string, unknown>, Date>((d) => xAccessor(d)).left,\n    [xAccessor]\n  );\n\n  const visiblePlotData = useMemo(() => {\n    if (!xDomain) {\n      return plotData;\n    }\n    return filterDataByXDomain(plotData, xDomain, xAccessor);\n  }, [plotData, xDomain, xAccessor]);\n\n  const projectionConfigs = useMemo(\n    () => extractProjectionLineConfigs(children),\n    [children]\n  );\n\n  const xScale = useMemo(() => {\n    const minTime = xDomain\n      ? xDomain[0].getTime()\n      : (extent(plotData, (d) => xAccessor(d).getTime())[0] ?? 0);\n    let maxTime = xDomain\n      ? xDomain[1].getTime()\n      : (extent(plotData, (d) => xAccessor(d).getTime())[1] ?? minTime);\n    // Brush defines the viewport — projection horizon is included via brush\n    // track extent, not by extending past the selection on the main chart.\n    if (!xDomain) {\n      maxTime = mergeProjectionXDomainMax(maxTime, projectionConfigs);\n    }\n\n    return scaleTime({\n      range: [0, innerWidth],\n      domain: [minTime, maxTime],\n    });\n  }, [innerWidth, plotData, projectionConfigs, xAccessor, xDomain]);\n\n  // When brushing, keep the full series for path rendering so edge fades stay\n  // anchored to the viewport while the line pans through them. Y-domain and\n  // interaction still use the filtered visible slice.\n  const seriesSourceData = xDomain ? plotData : visiblePlotData;\n\n  const renderData = useMemo(() => {\n    const valueKeys = lines.map((line) => line.dataKey);\n    return decimateTimeSeries(\n      seriesSourceData,\n      maxRenderPointsForWidth(innerWidth),\n      valueKeys\n    );\n  }, [seriesSourceData, innerWidth, lines]);\n\n  const columnWidth = useMemo(() => {\n    const slotCount =\n      xDomain && xDomainSlotCount != null\n        ? xDomainSlotCount\n        : visiblePlotData.length;\n    if (slotCount < 2) {\n      return 0;\n    }\n    return innerWidth / (slotCount - 1);\n  }, [innerWidth, visiblePlotData.length, xDomain, xDomainSlotCount]);\n\n  const yDomainSkeletonByAxis = useMemo(\n    () =>\n      computeYDomainsByAxis({\n        lines,\n        resolveDomain: (dataKeys) => resolveYDomain(skeletonData, dataKeys),\n      }),\n    [lines, resolveYDomain, skeletonData]\n  );\n\n  const yDomainTargetByAxis = useMemo(() => {\n    const base = computeYDomainsByAxis({\n      lines,\n      resolveDomain: (dataKeys) =>\n        resolveYDomain(xDomain ? visiblePlotData : data, dataKeys),\n    });\n    if (projectionConfigs.length === 0) {\n      return base;\n    }\n    const merged: Record<string, [number, number]> = { ...base };\n    for (const axisId of Object.keys(base)) {\n      merged[axisId] = mergeProjectionYDomain(\n        base[axisId] ?? [0, 100],\n        projectionConfigs,\n        axisId\n      );\n    }\n    for (const config of projectionConfigs) {\n      if (!merged[config.yAxisId]) {\n        merged[config.yAxisId] = mergeProjectionYDomain(\n          [0, 100],\n          projectionConfigs,\n          config.yAxisId\n        );\n      }\n    }\n    return merged;\n  }, [\n    data,\n    lines,\n    projectionConfigs,\n    resolveYDomain,\n    visiblePlotData,\n    xDomain,\n  ]);\n\n  const animatedYDomainsByAxis = useAnimatedYDomains({\n    chartPhase,\n    durationMs: yDomainTweenDuration,\n    enabled: yDomainTween,\n    onSettled: notifyYDomainTweenComplete,\n    skeletonByAxis: yDomainSkeletonByAxis,\n    targetByAxis: yDomainTargetByAxis,\n    tweenOnTargetChange:\n      yDomainTween || (tweenYDomainOnXDomainChange && xDomain != null),\n  });\n\n  const yDomainsForScales = animatedYDomainsByAxis;\n\n  const yScales = useMemo(\n    () =>\n      buildYScalesFromDomains({\n        domainsByAxis: yDomainsForScales,\n        innerHeight,\n        lines,\n      }),\n    [yDomainsForScales, innerHeight, lines]\n  );\n\n  const yScale = getPrimaryYScale(\n    yScales,\n    scaleLinear({ range: [innerHeight, 0], domain: [0, 100], nice: true })\n  );\n\n  const dateLabels = useMemo(\n    () => visiblePlotData.map((d) => shortDateFmt.format(xAccessor(d))),\n    [visiblePlotData, xAccessor]\n  );\n\n  const canInteract = isLoaded && isChartInteractionPhase(chartPhase);\n\n  const {\n    tooltipData,\n    setTooltipData,\n    selection,\n    clearSelection,\n    interactionHandlers,\n    interactionStyle,\n  } = useChartInteraction({\n    bisectDate,\n    canInteract,\n    data: visiblePlotData,\n    lines,\n    margin,\n    xAccessor,\n    xScale,\n    yScale,\n    yScales,\n  });\n\n  const defsChildren: ReactElement[] = [];\n  const clipExcludedChildren: ReactElement[] = [];\n  const underlayChildren: ReactElement[] = [];\n  const preOverlayChildren: ReactElement[] = [];\n  const postOverlayChildren: ReactElement[] = [];\n\n  Children.forEach(children, (child, index) => {\n    if (!isValidElement(child)) {\n      return;\n    }\n\n    const keyedChild = ensureChildKey(child, index);\n    const resolvedChild = resolveChartChildElement(keyedChild);\n\n    if (isGradientDefComponent(resolvedChild)) {\n      defsChildren.push(resolvedChild);\n    } else if (isPatternDefComponent(resolvedChild)) {\n      preOverlayChildren.push(resolvedChild);\n    } else if (isPostOverlayComponent(resolvedChild)) {\n      postOverlayChildren.push(resolvedChild);\n    } else if (isClipExcludedComponent(resolvedChild)) {\n      clipExcludedChildren.push(resolvedChild);\n    } else if (isUnderlayComponent(resolvedChild)) {\n      underlayChildren.push(resolvedChild);\n    } else {\n      preOverlayChildren.push(resolvedChild);\n    }\n  });\n\n  const [registeredReferenceAreas, setRegisteredReferenceAreas] = useState(\n    () => new Map<string, ReferenceAreaConfig>()\n  );\n\n  const registerReferenceArea = useCallback(\n    (id: string, config: ReferenceAreaConfig) => {\n      setRegisteredReferenceAreas((prev) => {\n        const existing = prev.get(id);\n        if (\n          existing &&\n          existing.yAxisId === config.yAxisId &&\n          existing.y1 === config.y1 &&\n          existing.y2 === config.y2 &&\n          existing.axisLabelColor === config.axisLabelColor\n        ) {\n          return prev;\n        }\n        const next = new Map(prev);\n        next.set(id, config);\n        return next;\n      });\n    },\n    []\n  );\n\n  const unregisterReferenceArea = useCallback((id: string) => {\n    setRegisteredReferenceAreas((prev) => {\n      if (!prev.has(id)) {\n        return prev;\n      }\n      const next = new Map(prev);\n      next.delete(id);\n      return next;\n    });\n  }, []);\n\n  const referenceAreaRegistration = useMemo(\n    () => ({ registerReferenceArea, unregisterReferenceArea }),\n    [registerReferenceArea, unregisterReferenceArea]\n  );\n\n  const referenceAreas = useMemo(() => {\n    const extracted = extractReferenceAreaConfigs(children);\n    const registered = [...registeredReferenceAreas.values()];\n    if (registered.length === 0) {\n      return extracted;\n    }\n    if (extracted.length === 0) {\n      return registered;\n    }\n    return [...extracted, ...registered];\n  }, [children, registeredReferenceAreas]);\n\n  const contextValue = useMemo(\n    () => ({\n      data: visiblePlotData,\n      renderData,\n      xScale,\n      yScale,\n      yScales,\n      width,\n      height,\n      innerWidth,\n      innerHeight,\n      margin,\n      columnWidth,\n      tooltipData,\n      setTooltipData,\n      containerRef,\n      lines,\n      referenceAreas,\n      chartPhase,\n      chartStatus,\n      loadingLabel,\n      yDomainTweenDuration,\n      yDomainSkeletonByAxis,\n      yDomainTargetByAxis,\n      isLoaded,\n      animationDuration,\n      animationEasing,\n      enterTransition,\n      revealEpoch,\n      notifyLoadingPulseComplete,\n      xAccessor,\n      dateLabels,\n      xDomain,\n      xDomainSlotCount,\n      selection,\n      clearSelection,\n      composedBarDataKeys,\n      composedBarSize,\n      composedMaxBarSize,\n      composedBarGap,\n      composedStacked,\n      composedStackOffsets,\n      composedStackGap,\n    }),\n    [\n      visiblePlotData,\n      renderData,\n      xScale,\n      yScale,\n      yScales,\n      width,\n      height,\n      innerWidth,\n      innerHeight,\n      margin,\n      columnWidth,\n      tooltipData,\n      setTooltipData,\n      containerRef,\n      lines,\n      referenceAreas,\n      chartPhase,\n      chartStatus,\n      loadingLabel,\n      yDomainTweenDuration,\n      yDomainSkeletonByAxis,\n      yDomainTargetByAxis,\n      isLoaded,\n      animationDuration,\n      animationEasing,\n      enterTransition,\n      revealEpoch,\n      notifyLoadingPulseComplete,\n      xAccessor,\n      dateLabels,\n      xDomain,\n      xDomainSlotCount,\n      selection,\n      clearSelection,\n      composedBarDataKeys,\n      composedBarSize,\n      composedMaxBarSize,\n      composedBarGap,\n      composedStacked,\n      composedStackOffsets,\n      composedStackGap,\n    ]\n  );\n\n  const useClipReveal =\n    !staticPreview &&\n    renderData.length > 1 &&\n    innerWidth > 0 &&\n    animationDuration > 0;\n  const isRevealAnimating = chartPhase === \"revealing\";\n  const isRevealConcealing =\n    chartPhase === \"exitingReady\" && animationDuration > 0;\n\n  const effectiveEnterTransition: Transition =\n    enterTransition ??\n    ({\n      ...DEFAULT_CHART_ENTER_TRANSITION,\n      duration: animationDuration / 1000,\n    } satisfies Transition);\n\n  const revealClipPadding = useMemo(() => {\n    if (!composedBarDataKeys?.length) {\n      return 0;\n    }\n    const barWidth = computeSeriesBarWidth({\n      columnWidth,\n      composedBarGap,\n      composedBarSize,\n      composedMaxBarSize,\n      dataLength: plotData.length,\n      innerWidth,\n      seriesCount: composedBarDataKeys.length,\n      stacked: composedStacked,\n    });\n    return computeSeriesBarRevealClipPadding({\n      barWidth,\n      gap: composedBarGap,\n      seriesCount: composedBarDataKeys.length,\n      stacked: composedStacked,\n    });\n  }, [\n    columnWidth,\n    composedBarDataKeys,\n    composedBarGap,\n    composedBarSize,\n    composedMaxBarSize,\n    composedStacked,\n    innerWidth,\n    plotData.length,\n  ]);\n\n  return (\n    <ReferenceAreaRegistrationContext.Provider\n      value={referenceAreaRegistration}\n    >\n      <ChartProvider value={contextValue}>\n        <svg aria-hidden=\"true\" height={height} width={width}>\n          <defs>\n            {defsChildren}\n            {useClipReveal ? (\n              <ChartRevealClip\n                animating={isRevealAnimating || isRevealConcealing}\n                clipPathId={clipPathId}\n                enterTransition={effectiveEnterTransition}\n                height={innerHeight + 20}\n                mode={isRevealConcealing ? \"conceal\" : \"reveal\"}\n                onComplete={\n                  isRevealConcealing ? notifyRevealConcealComplete : undefined\n                }\n                padding={revealClipPadding}\n                revealEpoch={isRevealConcealing ? concealEpoch : revealEpoch}\n                targetWidth={innerWidth}\n              />\n            ) : null}\n          </defs>\n\n          <rect fill=\"transparent\" height={height} width={width} x={0} y={0} />\n\n          <g\n            {...interactionHandlers}\n            style={interactionStyle}\n            transform={`translate(${margin.left},${margin.top})`}\n          >\n            <rect\n              fill=\"transparent\"\n              height={innerHeight}\n              width={innerWidth}\n              x={0}\n              y={0}\n            />\n\n            {clipExcludedChildren}\n            {underlayChildren}\n            {useClipReveal ? (\n              <g clipPath={`url(#${clipPathId})`}>{preOverlayChildren}</g>\n            ) : (\n              preOverlayChildren\n            )}\n            {postOverlayChildren}\n          </g>\n        </svg>\n      </ChartProvider>\n    </ReferenceAreaRegistrationContext.Provider>\n  );\n});\n",
      "type": "registry:component",
      "target": "components/charts/time-series-chart-shell.tsx"
    },
    {
      "path": "src/charts/reference-area-registration-context.tsx",
      "content": "\"use client\";\n\nimport { createContext, useContext } from \"react\";\nimport type { ReferenceAreaConfig } from \"./reference-area-config\";\n\nexport interface ReferenceAreaRegistrationContextValue {\n  registerReferenceArea: (id: string, config: ReferenceAreaConfig) => void;\n  unregisterReferenceArea: (id: string) => void;\n}\n\nexport const ReferenceAreaRegistrationContext =\n  createContext<ReferenceAreaRegistrationContextValue | null>(null);\n\nexport function useReferenceAreaRegistration(): ReferenceAreaRegistrationContextValue | null {\n  return useContext(ReferenceAreaRegistrationContext);\n}\n",
      "type": "registry:component",
      "target": "components/charts/reference-area-registration-context.tsx"
    },
    {
      "path": "src/charts/projection-config.ts",
      "content": "import {\n  Children,\n  Fragment,\n  isValidElement,\n  type ReactElement,\n  type ReactNode,\n} from \"react\";\nimport { isChartClipPassthrough } from \"./chart-child-passthrough\";\nimport type { ProjectionPoint } from \"./projection-utils\";\nimport {\n  projectionDateExtents,\n  projectionValueExtents,\n} from \"./projection-utils\";\nimport { normalizeYAxisId } from \"./y-axis-scales\";\n\nexport interface ProjectionLineConfig {\n  yAxisId: string;\n  data: ProjectionPoint[];\n}\n\ninterface ProjectionLineConfigProps {\n  data?: ProjectionPoint[];\n  yAxisId?: string | number;\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 isProjectionLineElement(child: ReactElement): boolean {\n  return getChildComponentName(child) === \"ProjectionLine\";\n}\n\nfunction normalizeProjectionData(\n  data: ProjectionPoint[] | undefined\n): ProjectionPoint[] {\n  if (!data?.length) {\n    return [];\n  }\n  return data.map((point) => ({\n    date: point.date instanceof Date ? point.date : new Date(point.date),\n    value: point.value,\n  }));\n}\n\n/** Collect {@link ProjectionLine} props from chart children for domain extension. */\nexport function extractProjectionLineConfigs(\n  children: ReactNode\n): ProjectionLineConfig[] {\n  const configs: ProjectionLineConfig[] = [];\n\n  const visit = (node: ReactNode) => {\n    Children.forEach(node, (child) => {\n      if (!isValidElement(child)) {\n        return;\n      }\n\n      if (child.type === Fragment) {\n        visit((child.props as { children?: ReactNode }).children);\n        return;\n      }\n\n      if (isProjectionLineElement(child)) {\n        const props = child.props as ProjectionLineConfigProps | undefined;\n        const data = normalizeProjectionData(props?.data);\n        if (data.length >= 2) {\n          configs.push({\n            yAxisId: normalizeYAxisId(props?.yAxisId),\n            data,\n          });\n        }\n        return;\n      }\n\n      if (isChartClipPassthrough(child.type)) {\n        visit((child.props as { children?: ReactNode }).children);\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\nexport function mergeProjectionYDomain(\n  domain: [number, number],\n  configs: ProjectionLineConfig[],\n  yAxisId: string\n): [number, number] {\n  const paths = configs\n    .filter((config) => config.yAxisId === yAxisId)\n    .map((config) => config.data);\n  const extents = projectionValueExtents(paths);\n  if (!extents) {\n    return domain;\n  }\n\n  const [min, max] = domain;\n  const nextMin = Math.min(min, extents.minValue);\n  const nextMax = Math.max(max, extents.maxValue);\n\n  if (nextMin >= 0 && min >= 0) {\n    return [0, nextMax <= 0 ? 100 : nextMax * 1.1];\n  }\n\n  const padding = (nextMax - nextMin) * 0.05 || 1;\n  return [nextMin - padding, nextMax + padding];\n}\n\nexport function mergeProjectionXDomainMax(\n  maxTime: number,\n  configs: ProjectionLineConfig[]\n): number {\n  const paths = configs.map((config) => config.data);\n  const extents = projectionDateExtents(paths);\n  if (!extents) {\n    return maxTime;\n  }\n  return Math.max(maxTime, extents.maxTime);\n}\n",
      "type": "registry:component",
      "target": "components/charts/projection-config.ts"
    },
    {
      "path": "src/charts/projection-utils.ts",
      "content": "export type ProjectionMode = \"auto\" | \"target\" | \"manual\";\nexport type ProjectionAutoMethod = \"linearRegression\" | \"lastSegment\";\n/** How the projection segment is drawn between anchor and horizon. */\nexport type ProjectionCurveKind = \"linear\" | \"bezier\";\n/** @deprecated Stepped density removed — projections always anchor → horizon. */\nexport type ProjectionPathDensity = \"stepped\" | \"endpoints\";\n\nexport interface ProjectionPoint {\n  date: Date;\n  value: number;\n}\n\nexport interface BuildProjectionPathOptions {\n  sourceData: Record<string, unknown>[];\n  seriesKey: string;\n  xDataKey?: string;\n  mode: ProjectionMode;\n  autoMethod?: ProjectionAutoMethod;\n  /** Auto mode: stepped points per interval, or anchor + end only. Default: stepped */\n  pathDensity?: ProjectionPathDensity;\n  /** Index in sourceData where projection anchors (default: last point). */\n  startIndex?: number;\n  /** How many future points to generate (matches source cadence). */\n  horizonPoints?: number;\n  /** Target Y at the final projected date (target mode). */\n  endValue?: number;\n  /** Full manual path — anchor + future points (manual mode). */\n  points?: ProjectionPoint[];\n}\n\nfunction readDate(row: Record<string, unknown>, xDataKey: string): Date | null {\n  const raw = row[xDataKey];\n  if (raw instanceof Date && !Number.isNaN(raw.getTime())) {\n    return raw;\n  }\n  if (typeof raw === \"number\" && Number.isFinite(raw)) {\n    const date = new Date(raw);\n    return Number.isNaN(date.getTime()) ? null : date;\n  }\n  if (typeof raw === \"string\") {\n    const date = new Date(raw);\n    return Number.isNaN(date.getTime()) ? null : date;\n  }\n  return null;\n}\n\nfunction readValue(\n  row: Record<string, unknown>,\n  seriesKey: string\n): number | null {\n  const raw = row[seriesKey];\n  return typeof raw === \"number\" && Number.isFinite(raw) ? raw : null;\n}\n\nfunction resolveStartIndex(\n  sourceData: Record<string, unknown>[],\n  startIndex: number | undefined\n): number {\n  if (startIndex == null || !Number.isFinite(startIndex)) {\n    return Math.max(0, sourceData.length - 1);\n  }\n  return Math.min(Math.max(0, Math.floor(startIndex)), sourceData.length - 1);\n}\n\nfunction intervalFromAdjacentRows(\n  sourceData: Record<string, unknown>[],\n  xDataKey: string,\n  startIndex: number\n): number | null {\n  if (startIndex < 1) {\n    return null;\n  }\n  const prevRow = sourceData[startIndex - 1];\n  const currentRow = sourceData[startIndex];\n  const prev = prevRow ? readDate(prevRow, xDataKey) : null;\n  const current = currentRow ? readDate(currentRow, xDataKey) : null;\n  if (!(prev && current)) {\n    return null;\n  }\n  const delta = current.getTime() - prev.getTime();\n  return delta > 0 ? delta : null;\n}\n\nfunction intervalFromSeriesSpan(\n  sourceData: Record<string, unknown>[],\n  xDataKey: string\n): number | null {\n  if (sourceData.length < 2) {\n    return null;\n  }\n  const firstRow = sourceData[0];\n  const lastRow = sourceData.at(-1);\n  const first = firstRow ? readDate(firstRow, xDataKey) : null;\n  const last = lastRow ? readDate(lastRow, xDataKey) : null;\n  if (!(first && last)) {\n    return null;\n  }\n  const span = last.getTime() - first.getTime();\n  return span > 0 ? span / (sourceData.length - 1) : null;\n}\n\nfunction resolveIntervalMs(\n  sourceData: Record<string, unknown>[],\n  xDataKey: string,\n  startIndex: number\n): number {\n  return (\n    intervalFromAdjacentRows(sourceData, xDataKey, startIndex) ??\n    intervalFromSeriesSpan(sourceData, xDataKey) ??\n    86_400_000\n  );\n}\n\nfunction linearRegressionSlope(points: { t: number; y: number }[]): number {\n  if (points.length < 2) {\n    return 0;\n  }\n  const n = points.length;\n  let sumT = 0;\n  let sumY = 0;\n  let sumTY = 0;\n  let sumTT = 0;\n  for (const { t, y } of points) {\n    sumT += t;\n    sumY += y;\n    sumTY += t * y;\n    sumTT += t * t;\n  }\n  const denom = n * sumTT - sumT * sumT;\n  if (Math.abs(denom) < 1e-12) {\n    return 0;\n  }\n  return (n * sumTY - sumT * sumY) / denom;\n}\n\nfunction buildAutoFutureValues(options: {\n  anchorTime: number;\n  anchorValue: number;\n  autoMethod: ProjectionAutoMethod;\n  historyPoints: { t: number; y: number }[];\n  horizonPoints: number;\n  intervalMs: number;\n  pathDensity: ProjectionPathDensity;\n}): ProjectionPoint[] {\n  const {\n    anchorTime,\n    anchorValue,\n    autoMethod,\n    historyPoints,\n    horizonPoints,\n    intervalMs,\n    pathDensity,\n  } = options;\n\n  const slope =\n    autoMethod === \"lastSegment\" && historyPoints.length >= 2\n      ? (() => {\n          const prev = historyPoints.at(-2);\n          const last = historyPoints.at(-1);\n          if (!(prev && last)) {\n            return 0;\n          }\n          const dt = last.t - prev.t;\n          return dt === 0 ? 0 : (last.y - prev.y) / dt;\n        })()\n      : linearRegressionSlope(historyPoints);\n\n  if (pathDensity === \"endpoints\") {\n    const endTime = anchorTime + intervalMs * horizonPoints;\n    const endValue = anchorValue + slope * intervalMs * horizonPoints;\n    return [\n      { date: new Date(anchorTime), value: anchorValue },\n      { date: new Date(endTime), value: endValue },\n    ];\n  }\n\n  const result: ProjectionPoint[] = [\n    { date: new Date(anchorTime), value: anchorValue },\n  ];\n\n  for (let i = 1; i <= horizonPoints; i++) {\n    const t = anchorTime + intervalMs * i;\n    const value = anchorValue + slope * intervalMs * i;\n    result.push({ date: new Date(t), value });\n  }\n\n  return result;\n}\n\n/** Slope (value change per ms) at the projection anchor from the last data segment. */\nexport function computeProjectionAnchorTangentSlope(\n  sourceData: Record<string, unknown>[],\n  seriesKey: string,\n  xDataKey = \"date\",\n  startIndexProp?: number\n): number {\n  if (sourceData.length < 2) {\n    return 0;\n  }\n  const startIndex = resolveStartIndex(sourceData, startIndexProp);\n  const historyPoints: { t: number; y: number }[] = [];\n  for (let i = 0; i <= startIndex; i++) {\n    const row = sourceData[i];\n    if (!row) {\n      continue;\n    }\n    const date = readDate(row, xDataKey);\n    const value = readValue(row, seriesKey);\n    if (date && value != null) {\n      historyPoints.push({ t: date.getTime(), y: value });\n    }\n  }\n  if (historyPoints.length < 2) {\n    return 0;\n  }\n  const prev = historyPoints.at(-2);\n  const last = historyPoints.at(-1);\n  if (!(prev && last)) {\n    return 0;\n  }\n  const dt = last.t - prev.t;\n  return dt === 0 ? 0 : (last.y - prev.y) / dt;\n}\n\n/** Cubic bezier with horizontal tangents at start and end (price-target S-curve). */\nexport function buildHorizontalTangentBezierPath(\n  x0: number,\n  y0: number,\n  x1: number,\n  y1: number,\n  /** How far control points sit along the x span (0–0.5). Default: 0.45 */\n  tension = 0.45\n): string {\n  const dx = x1 - x0;\n  if (Math.abs(dx) < 1e-6) {\n    return `M ${x0},${y0} L ${x1},${y1}`;\n  }\n  const t = Math.min(0.5, Math.max(0.05, tension));\n  const c1x = x0 + dx * t;\n  const c2x = x1 - dx * t;\n  return `M ${x0},${y0} C ${c1x},${y0} ${c2x},${y1} ${x1},${y1}`;\n}\n\nfunction buildTargetPath(options: {\n  anchorTime: number;\n  anchorValue: number;\n  endValue: number;\n  horizonPoints: number;\n  intervalMs: number;\n}): ProjectionPoint[] {\n  const { anchorTime, anchorValue, endValue, horizonPoints, intervalMs } =\n    options;\n  const endTime = anchorTime + intervalMs * horizonPoints;\n  return [\n    { date: new Date(anchorTime), value: anchorValue },\n    { date: new Date(endTime), value: endValue },\n  ];\n}\n\n/** Build a projection path from historical chart data or explicit points. */\nexport function buildProjectionPath(\n  options: BuildProjectionPathOptions\n): ProjectionPoint[] {\n  const {\n    sourceData,\n    seriesKey,\n    xDataKey = \"date\",\n    mode,\n    autoMethod = \"linearRegression\",\n    pathDensity = \"endpoints\",\n    startIndex: startIndexProp,\n    horizonPoints = 6,\n    endValue,\n    points,\n  } = options;\n\n  if (mode === \"manual\" && points && points.length >= 2) {\n    return points.map((point) => ({\n      date: new Date(point.date),\n      value: point.value,\n    }));\n  }\n\n  if (sourceData.length === 0) {\n    return [];\n  }\n\n  const startIndex = resolveStartIndex(sourceData, startIndexProp);\n  const anchorRow = sourceData[startIndex];\n  if (!anchorRow) {\n    return [];\n  }\n\n  const anchorDate = readDate(anchorRow, xDataKey);\n  const anchorValue = readValue(anchorRow, seriesKey);\n  if (!anchorDate || anchorValue == null) {\n    return [];\n  }\n\n  const intervalMs = resolveIntervalMs(sourceData, xDataKey, startIndex);\n  const anchorTime = anchorDate.getTime();\n\n  const historyPoints: { t: number; y: number }[] = [];\n  for (let i = 0; i <= startIndex; i++) {\n    const row = sourceData[i];\n    if (!row) {\n      continue;\n    }\n    const date = readDate(row, xDataKey);\n    const value = readValue(row, seriesKey);\n    if (date && value != null) {\n      historyPoints.push({ t: date.getTime(), y: value });\n    }\n  }\n\n  if (mode === \"target\" && endValue != null && Number.isFinite(endValue)) {\n    return buildTargetPath({\n      anchorTime,\n      anchorValue,\n      endValue,\n      horizonPoints,\n      intervalMs,\n    });\n  }\n\n  return buildAutoFutureValues({\n    anchorTime,\n    anchorValue,\n    autoMethod,\n    historyPoints,\n    horizonPoints,\n    intervalMs,\n    pathDensity,\n  });\n}\n\n/** Collect numeric Y extents from projection point arrays. */\nexport function projectionValueExtents(\n  paths: ProjectionPoint[][]\n): { minValue: number; maxValue: number } | null {\n  let minValue = Number.POSITIVE_INFINITY;\n  let maxValue = Number.NEGATIVE_INFINITY;\n\n  for (const path of paths) {\n    for (const point of path) {\n      if (point.value < minValue) {\n        minValue = point.value;\n      }\n      if (point.value > maxValue) {\n        maxValue = point.value;\n      }\n    }\n  }\n\n  if (minValue === Number.POSITIVE_INFINITY) {\n    return null;\n  }\n\n  return { minValue, maxValue };\n}\n\n/** Collect date extents from projection point arrays. */\nexport function projectionDateExtents(\n  paths: ProjectionPoint[][]\n): { minTime: number; maxTime: number } | null {\n  let minTime = Number.POSITIVE_INFINITY;\n  let maxTime = Number.NEGATIVE_INFINITY;\n\n  for (const path of paths) {\n    for (const point of path) {\n      const time = point.date.getTime();\n      if (time < minTime) {\n        minTime = time;\n      }\n      if (time > maxTime) {\n        maxTime = time;\n      }\n    }\n  }\n\n  if (minTime === Number.POSITIVE_INFINITY) {\n    return null;\n  }\n\n  return { minTime, maxTime };\n}\n",
      "type": "registry:component",
      "target": "components/charts/projection-utils.ts"
    },
    {
      "path": "src/charts/chart-child-passthrough.ts",
      "content": "import {\n  Children,\n  cloneElement,\n  Fragment,\n  isValidElement,\n  type ReactElement,\n  type ReactNode,\n} from \"react\";\n\n/** Marker on wrapper components whose single child should inherit clip classification. */\nexport const CHART_CLIP_PASSTHROUGH = \"__chartClipPassthrough\" as const;\n\nexport function isChartClipPassthrough(type: unknown): boolean {\n  return (\n    typeof type === \"function\" &&\n    (type as { [CHART_CLIP_PASSTHROUGH]?: boolean })[CHART_CLIP_PASSTHROUGH] ===\n      true\n  );\n}\n\n/** Unwrap visibility wrappers so `Grid` / axes stay outside the series clip. */\nexport function resolveChartChildElement(child: ReactElement): ReactElement {\n  if (isChartClipPassthrough(child.type)) {\n    const inner = (child.props as { children?: unknown }).children;\n    if (isValidElement(inner)) {\n      return resolveChartChildElement(inner);\n    }\n  }\n  return child;\n}\n\n/** Walk chart children, flattening React fragments (studio often groups layers in `<>...</>`). */\nexport function forEachChartChild(\n  children: ReactNode,\n  callback: (child: ReactElement, index: number) => void\n) {\n  let index = 0;\n  const visit = (nodes: ReactNode) => {\n    Children.forEach(nodes, (child) => {\n      if (!isValidElement(child)) {\n        return;\n      }\n      if (child.type === Fragment) {\n        visit((child.props as { children?: ReactNode }).children);\n        return;\n      }\n      callback(child, index);\n      index += 1;\n    });\n  };\n  visit(children);\n}\n\nconst CLIP_EXCLUDED_COMPONENT_NAMES = new Set([\n  \"Background\",\n  \"Grid\",\n  \"XAxis\",\n  \"YAxis\",\n  \"BarXAxis\",\n  \"BarYAxis\",\n  \"LiveXAxis\",\n  \"LiveYAxis\",\n]);\n\nconst UNDERLAY_COMPONENT_NAMES = new Set([\"ReferenceArea\", \"BarColumnTrack\"]);\n\n/** Markers render after the interaction overlay so they stay clickable. */\nexport function isPostOverlayComponent(child: ReactElement): boolean {\n  const childType = child.type as {\n    displayName?: string;\n    name?: string;\n    __isChartMarkers?: boolean;\n    __isPostOverlay?: boolean;\n  };\n\n  if (childType.__isChartMarkers || childType.__isPostOverlay) {\n    return true;\n  }\n\n  const componentName =\n    typeof child.type === \"function\"\n      ? childType.displayName || childType.name || \"\"\n      : \"\";\n\n  return (\n    componentName === \"ChartMarkers\" ||\n    componentName === \"MarkerGroup\" ||\n    componentName === \"ChartBrush\"\n  );\n}\n\n/** Renders above grid/axes but below series; excluded from grow-clip reveal. */\nexport function isUnderlayComponent(child: ReactElement): boolean {\n  const childType = child.type as { displayName?: string; name?: string };\n  const componentName =\n    typeof child.type === \"function\"\n      ? childType.displayName || childType.name || \"\"\n      : \"\";\n  return UNDERLAY_COMPONENT_NAMES.has(componentName);\n}\n\n/** Grid and axes stay visible during series clip reveal (e.g. loading → ready). */\nexport function isClipExcludedComponent(child: ReactElement): boolean {\n  const childType = child.type as { displayName?: string; name?: string };\n  const componentName =\n    typeof child.type === \"function\"\n      ? childType.displayName || childType.name || \"\"\n      : \"\";\n  return CLIP_EXCLUDED_COMPONENT_NAMES.has(componentName);\n}\n\n/** SVG layer lists from chart shells need stable keys when rendered as arrays. */\nexport function renderKeyedChartLayers(children: ReactElement[]) {\n  return children.map((child, index) =>\n    cloneElement(child, { key: child.key ?? `chart-layer-${index}` })\n  );\n}\n",
      "type": "registry:component",
      "target": "components/charts/chart-child-passthrough.ts"
    }
  ]
}