{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "scatter-chart",
  "type": "registry:component",
  "title": "Scatter Chart",
  "description": "A composable time-series scatter chart with offset rings, hover dimming, and animated enter",
  "dependencies": [
    "d3-array",
    "d3-scale",
    "motion",
    "react-use-measure"
  ],
  "registryDependencies": [
    "@bklit/chart-context",
    "@bklit/chart-animation",
    "@bklit/chart-series",
    "@bklit/grid",
    "@bklit/x-axis",
    "@bklit/chart-tooltip",
    "@bklit/utils"
  ],
  "files": [
    {
      "path": "src/charts/scatter-chart.tsx",
      "content": "\"use client\";\n\nimport type { Transition } from \"motion/react\";\nimport {\n  Children,\n  isValidElement,\n  type ReactNode,\n  useMemo,\n  useRef,\n} from \"react\";\nimport useMeasure from \"react-use-measure\";\nimport { cn } from \"@/lib/utils\";\nimport { DEFAULT_CHART_ENTER_TRANSITION } from \"./animation\";\nimport {\n  defaultScatterColors,\n  type LineConfig,\n  type Margin,\n} from \"./chart-context\";\nimport type { ChartPhase } from \"./chart-phase\";\nimport { Scatter, type ScatterProps } from \"./scatter\";\nimport { ScatterChartInner } from \"./scatter-chart-shell\";\n\nexport interface ScatterChartProps {\n  /** Data array — each item should have a date field and numeric values */\n  data: Record<string, unknown>[];\n  /** Key in data for the x-axis (date). Default: \"date\" */\n  xDataKey?: string;\n  /** Chart margins */\n  margin?: Partial<Margin>;\n  /** Animation duration in milliseconds. Default: 1100 */\n  animationDuration?: number;\n  /** CSS easing for clip-reveal. Default: cubic-bezier(0.85, 0, 0.15, 1) */\n  animationEasing?: string;\n  enterTransition?: Transition;\n  revealSignature?: string;\n  /** Aspect ratio as \"width / height\". Default: \"2 / 1\" */\n  aspectRatio?: string;\n  /** Additional class name for the container */\n  className?: string;\n  /** Child components (Scatter, Grid, ChartTooltip, XAxis, etc.) */\n  children: ReactNode;\n  onPhaseChange?: (phase: ChartPhase) => void;\n}\n\nconst DEFAULT_MARGIN: Margin = { top: 40, right: 40, bottom: 40, left: 40 };\n\nfunction extractScatterConfigs(children: ReactNode): LineConfig[] {\n  const configs: LineConfig[] = [];\n  let seriesIndex = 0;\n\n  Children.forEach(children, (child) => {\n    if (!isValidElement(child)) {\n      return;\n    }\n\n    const childType = child.type as {\n      displayName?: string;\n      name?: string;\n    };\n    const componentName =\n      typeof child.type === \"function\"\n        ? childType.displayName || childType.name || \"\"\n        : \"\";\n\n    const props = child.props as ScatterProps | undefined;\n    const isScatterComponent =\n      componentName === \"Scatter\" ||\n      child.type === Scatter ||\n      (props && typeof props.dataKey === \"string\" && props.dataKey.length > 0);\n\n    if (isScatterComponent && props?.dataKey) {\n      const seriesColor =\n        defaultScatterColors[seriesIndex % defaultScatterColors.length] ??\n        defaultScatterColors[0];\n      configs.push({\n        dataKey: props.dataKey,\n        stroke: props.fill || props.stroke || seriesColor,\n        strokeWidth: props.radius ?? 5,\n        yAxisId: props.yAxisId,\n      });\n      seriesIndex += 1;\n    }\n  });\n\n  return configs;\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  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  onPhaseChange,\n}: ChartInnerProps) {\n  const lines = useMemo(() => extractScatterConfigs(children), [children]);\n\n  return (\n    <ScatterChartInner\n      animationDuration={animationDuration}\n      animationEasing={animationEasing}\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    >\n      {children}\n    </ScatterChartInner>\n  );\n}\n\nexport function ScatterChart({\n  data,\n  xDataKey = \"date\",\n  margin: marginProp,\n  animationDuration = 1100,\n  animationEasing,\n  enterTransition = DEFAULT_CHART_ENTER_TRANSITION,\n  revealSignature,\n  aspectRatio = \"2 / 1\",\n  className = \"\",\n  children,\n  onPhaseChange,\n}: ScatterChartProps) {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const margin = { ...DEFAULT_MARGIN, ...marginProp };\n  const [measureRef, bounds] = useMeasure({ debounce: 10 });\n\n  const setContainerRef = (node: HTMLDivElement | null) => {\n    containerRef.current = node;\n    measureRef(node);\n  };\n\n  const width = bounds.width ?? 0;\n  const height = bounds.height ?? 0;\n\n  return (\n    <div\n      className={cn(\"relative w-full\", className)}\n      ref={setContainerRef}\n      style={{ aspectRatio, touchAction: \"none\" }}\n    >\n      {width > 0 && height > 0 ? (\n        <ChartInner\n          animationDuration={animationDuration}\n          animationEasing={animationEasing}\n          containerRef={containerRef}\n          data={data}\n          enterTransition={enterTransition}\n          height={height}\n          margin={margin}\n          onPhaseChange={onPhaseChange}\n          revealSignature={revealSignature}\n          width={width}\n          xDataKey={xDataKey}\n        >\n          {children}\n        </ChartInner>\n      ) : null}\n    </div>\n  );\n}\n\nScatterChart.displayName = \"ScatterChart\";\n\nexport { Scatter, type ScatterProps } from \"./scatter\";\n\nexport default ScatterChart;\n",
      "type": "registry:component",
      "target": "components/charts/scatter-chart.tsx"
    },
    {
      "path": "src/charts/scatter-chart-shell.tsx",
      "content": "\"use client\";\n\nimport { bisector } from \"d3-array\";\nimport { scaleLinear, scaleTime } from \"d3-scale\";\nimport type { Transition } from \"motion/react\";\nimport {\n  Children,\n  isValidElement,\n  type ReactElement,\n  type ReactNode,\n  useCallback,\n  useEffect,\n  useMemo,\n  useState,\n} from \"react\";\nimport { DEFAULT_ANIMATION_EASING } from \"./animation\";\nimport {\n  isClipExcludedComponent,\n  isPostOverlayComponent,\n  isUnderlayComponent,\n} from \"./chart-child-passthrough\";\nimport {\n  type ChartContextValue,\n  ChartProvider,\n  type LineConfig,\n  type Margin,\n} from \"./chart-context\";\nimport { isGradientDefComponent, isPatternDefComponent } from \"./chart-defs\";\nimport { shortDateFmt } from \"./chart-formatters\";\nimport { type ChartPhase, DEFAULT_CHART_LIFECYCLE } from \"./chart-phase\";\nimport { extractReferenceAreaConfigs } from \"./reference-area-config\";\nimport { useScatterChartInteraction } from \"./use-scatter-chart-interaction\";\nimport { buildYScalesForLines, getPrimaryYScale } from \"./y-axis-scales\";\n\nexport interface ScatterChartInnerProps {\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  lines: LineConfig[];\n  onPhaseChange?: (phase: ChartPhase) => void;\n}\n\nexport function ScatterChartInner({\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  onPhaseChange,\n}: ScatterChartInnerProps) {\n  const [isLoaded, setIsLoaded] = useState(false);\n  const [revealEpoch, setRevealEpoch] = useState(0);\n\n  const innerWidth = width - margin.left - margin.right;\n  const innerHeight = height - margin.top - margin.bottom;\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 xRangePadding = useMemo(() => {\n    if (lines.length === 0) {\n      return 12;\n    }\n    const maxRadius = Math.max(...lines.map((line) => line.strokeWidth ?? 5));\n    return maxRadius + 10;\n  }, [lines]);\n\n  const xScale = useMemo(() => {\n    const dates = data.map((d) => xAccessor(d));\n    const minTime = Math.min(...dates.map((d) => d.getTime()));\n    const maxTime = Math.max(...dates.map((d) => d.getTime()));\n\n    return scaleTime<number>()\n      .range([\n        xRangePadding,\n        Math.max(xRangePadding, innerWidth - xRangePadding),\n      ])\n      .domain([minTime, maxTime]);\n  }, [innerWidth, data, xAccessor, xRangePadding]);\n\n  const columnWidth = useMemo(() => {\n    if (data.length < 2) {\n      return 0;\n    }\n    return innerWidth / (data.length - 1);\n  }, [innerWidth, data.length]);\n\n  const yScales = useMemo(\n    () =>\n      buildYScalesForLines({\n        lines,\n        data,\n        innerHeight,\n        resolveDomain: (dataKeys) => {\n          let maxValue = 0;\n          for (const d of data) {\n            for (const key of dataKeys) {\n              const value = d[key];\n              if (typeof value === \"number\" && value > maxValue) {\n                maxValue = value;\n              }\n            }\n          }\n          const top = maxValue <= 0 ? 100 : maxValue * 1.1;\n          return [0, top];\n        },\n      }),\n    [innerHeight, data, lines]\n  );\n\n  const yScale = getPrimaryYScale(\n    yScales,\n    scaleLinear<number>().range([innerHeight, 0]).domain([0, 100])\n  );\n\n  const dateLabels = useMemo(\n    () => data.map((d) => shortDateFmt.format(xAccessor(d))),\n    [data, xAccessor]\n  );\n\n  // biome-ignore lint/correctness/useExhaustiveDependencies: revealSignature\n  useEffect(() => {\n    setRevealEpoch((n) => n + 1);\n    setIsLoaded(false);\n    const timer = setTimeout(() => {\n      setIsLoaded(true);\n    }, animationDuration);\n    return () => clearTimeout(timer);\n  }, [animationDuration, revealSignature]);\n\n  useEffect(() => {\n    onPhaseChange?.(isLoaded ? \"ready\" : \"revealing\");\n  }, [isLoaded, onPhaseChange]);\n\n  const canInteract = isLoaded;\n\n  const {\n    tooltipData,\n    setTooltipData,\n    selection,\n    clearSelection,\n    interactionHandlers,\n    interactionStyle,\n  } = useScatterChartInteraction({\n    xScale,\n    yScale: yScale as ChartContextValue[\"yScale\"],\n    yScales: yScales as ChartContextValue[\"yScales\"],\n    data,\n    lines,\n    margin,\n    xAccessor,\n    bisectDate,\n    canInteract,\n  });\n\n  const referenceAreas = useMemo(\n    () => extractReferenceAreaConfigs(children),\n    [children]\n  );\n\n  if (width < 10 || height < 10) {\n    return null;\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) => {\n    if (!isValidElement(child)) {\n      return;\n    }\n\n    if (isGradientDefComponent(child)) {\n      defsChildren.push(child);\n    } else if (isPatternDefComponent(child)) {\n      preOverlayChildren.push(child);\n    } else if (isPostOverlayComponent(child)) {\n      postOverlayChildren.push(child);\n    } else if (isClipExcludedComponent(child)) {\n      clipExcludedChildren.push(child);\n    } else if (isUnderlayComponent(child)) {\n      underlayChildren.push(child);\n    } else {\n      preOverlayChildren.push(child);\n    }\n  });\n\n  const contextValue: ChartContextValue = {\n    ...DEFAULT_CHART_LIFECYCLE,\n    data,\n    renderData: data,\n    xScale: xScale as ChartContextValue[\"xScale\"],\n    yScale: yScale as ChartContextValue[\"yScale\"],\n    yScales: yScales as ChartContextValue[\"yScales\"],\n    width,\n    height,\n    innerWidth,\n    innerHeight,\n    margin,\n    columnWidth,\n    tooltipData,\n    setTooltipData,\n    containerRef,\n    lines,\n    referenceAreas,\n    isLoaded,\n    animationDuration,\n    animationEasing,\n    enterTransition,\n    revealEpoch,\n    xAccessor,\n    dateLabels,\n    selection,\n    clearSelection,\n  };\n\n  return (\n    <ChartProvider value={contextValue}>\n      <svg\n        aria-hidden=\"true\"\n        className=\"overflow-visible\"\n        height={height}\n        width={width}\n      >\n        {defsChildren.length > 0 && <defs>{defsChildren}</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          {preOverlayChildren}\n          {postOverlayChildren}\n        </g>\n      </svg>\n    </ChartProvider>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/charts/scatter-chart-shell.tsx"
    },
    {
      "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"
    },
    {
      "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/scatter.tsx",
      "content": "\"use client\";\n\nimport { useId } from \"react\";\nimport { useChartStable } from \"./chart-context\";\nimport { SeriesMarkers, type SeriesMarkersProps } from \"./series-markers\";\n\nexport interface ScatterProps extends Omit<SeriesMarkersProps, \"animate\"> {\n  /** Y-scale group id (Recharts `yAxisId`). Default: `\"left\"`. */\n  yAxisId?: string | number;\n  /** Whether to animate points with clip reveal. Default: true */\n  animate?: boolean;\n  /**\n   * Color each dot by its vertical position using a chart-space linear gradient.\n   * Lower values use `from`; higher values use `to`. Default stops: red (bottom) → green (top).\n   */\n  yGradient?: boolean | { from?: string; to?: string };\n}\n\nconst DEFAULT_Y_GRADIENT_FROM = \"var(--color-red-500)\";\nconst DEFAULT_Y_GRADIENT_TO = \"var(--color-emerald-500)\";\n\nexport function Scatter({\n  dataKey,\n  fill,\n  stroke,\n  strokeWidth = 2,\n  ringGap = 2,\n  outlineWidth = 0,\n  outlineColor,\n  radius = 5,\n  animate = true,\n  fadeOnHover = true,\n  inactiveOpacity = 0.5,\n  inactiveBlur = 2,\n  enterBlur = 2,\n  showActiveHighlight = true,\n  yGradient,\n}: ScatterProps) {\n  const { innerHeight } = useChartStable();\n\n  const yGradientConfig = (() => {\n    if (!yGradient) {\n      return null;\n    }\n    if (yGradient === true) {\n      return { from: DEFAULT_Y_GRADIENT_FROM, to: DEFAULT_Y_GRADIENT_TO };\n    }\n    return {\n      from: yGradient.from ?? DEFAULT_Y_GRADIENT_FROM,\n      to: yGradient.to ?? DEFAULT_Y_GRADIENT_TO,\n    };\n  })();\n\n  const yGradientId = `scatter-y-gradient-${useId().replace(/:/g, \"\")}`;\n  const gradientFill = yGradientConfig ? `url(#${yGradientId})` : undefined;\n\n  const resolvedFill = gradientFill ?? fill;\n  const resolvedStroke = stroke ?? (gradientFill ? gradientFill : undefined);\n\n  return (\n    <>\n      {yGradientConfig ? (\n        <defs>\n          <linearGradient\n            gradientUnits=\"userSpaceOnUse\"\n            id={yGradientId}\n            x1={0}\n            x2={0}\n            y1={innerHeight}\n            y2={0}\n          >\n            <stop offset=\"0%\" stopColor={yGradientConfig.from} />\n            <stop offset=\"100%\" stopColor={yGradientConfig.to} />\n          </linearGradient>\n        </defs>\n      ) : null}\n      <SeriesMarkers\n        animate={animate}\n        dataKey={dataKey}\n        enterBlur={enterBlur}\n        fadeOnHover={fadeOnHover}\n        fill={resolvedFill}\n        inactiveBlur={inactiveBlur}\n        inactiveOpacity={inactiveOpacity}\n        outlineColor={outlineColor}\n        outlineWidth={outlineWidth}\n        radius={radius}\n        ringGap={ringGap}\n        showActiveHighlight={showActiveHighlight}\n        stroke={resolvedStroke}\n        strokeWidth={strokeWidth}\n      />\n    </>\n  );\n}\n\nScatter.displayName = \"Scatter\";\n\nexport default Scatter;\n",
      "type": "registry:component",
      "target": "components/charts/scatter.tsx"
    },
    {
      "path": "src/charts/use-scatter-chart-interaction.ts",
      "content": "\"use client\";\n\nimport type { ScaleLinear, ScaleTime } from \"d3-scale\";\nimport { useCallback, useRef, useState } from \"react\";\nimport type { LineConfig, Margin, TooltipData } from \"./chart-context\";\nimport { localPointFromSvg } from \"./scatter-svg\";\nimport type { ChartSelection } from \"./use-chart-interaction\";\nimport { useScheduledTooltip } from \"./use-scheduled-tooltip\";\nimport { normalizeYAxisId } from \"./y-axis-scales\";\n\ntype XScale = ScaleTime<number, number>;\ntype YScale = ScaleLinear<number, number>;\n\ninterface UseScatterChartInteractionParams {\n  xScale: XScale;\n  yScale: YScale;\n  yScales: Record<string, YScale>;\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 ScatterChartInteractionResult {\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 useScatterChartInteraction({\n  xScale,\n  yScale,\n  yScales,\n  data,\n  lines,\n  margin,\n  xAccessor,\n  bisectDate,\n  canInteract,\n}: UseScatterChartInteractionParams): ScatterChartInteractionResult {\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\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      const svg = event.currentTarget.ownerSVGElement;\n      let clientX: number;\n      let clientY: number;\n\n      if (\"touches\" in event) {\n        const touch = event.touches[touchIndex];\n        if (!touch) {\n          return null;\n        }\n        clientX = touch.clientX;\n        clientY = touch.clientY;\n      } else {\n        clientX = event.clientX;\n        clientY = event.clientY;\n      }\n\n      const point = localPointFromSvg(svg, clientX, clientY);\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      const tooltip = resolveTooltipFromX(chartX);\n      if (tooltip) {\n        scheduleTooltip(tooltip);\n      }\n    },\n    [getChartX, resolveTooltipFromX, resolveIndexFromX, scheduleTooltip]\n  );\n\n  const handleMouseLeave = useCallback(() => {\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        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        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  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-scatter-chart-interaction.ts"
    },
    {
      "path": "src/charts/scatter-svg.ts",
      "content": "/** Map viewport coordinates to SVG user space (no @visx/event). */\nexport function localPointFromSvg(\n  svg: SVGSVGElement | null,\n  clientX: number,\n  clientY: number\n): { x: number; y: number } | null {\n  if (!svg) {\n    return null;\n  }\n\n  const point = svg.createSVGPoint();\n  point.x = clientX;\n  point.y = clientY;\n\n  const matrix = svg.getScreenCTM();\n  if (!matrix) {\n    return null;\n  }\n\n  const transformed = point.matrixTransform(matrix.inverse());\n  return { x: transformed.x, y: transformed.y };\n}\n",
      "type": "registry:component",
      "target": "components/charts/scatter-svg.ts"
    }
  ]
}