{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "area-chart",
  "type": "registry:component",
  "title": "Area Chart",
  "description": "A composable area chart with gradient fills and animations",
  "dependencies": [
    "@visx/curve@4.0.1-alpha.0",
    "@visx/gradient@4.0.1-alpha.0",
    "@visx/shape@4.0.1-alpha.0",
    "motion"
  ],
  "registryDependencies": [
    "@bklit/chart-context",
    "@bklit/chart-animation",
    "@bklit/chart-series",
    "@bklit/grid",
    "@bklit/x-axis",
    "@bklit/chart-tooltip",
    "@bklit/shimmering-text",
    "@bklit/utils"
  ],
  "files": [
    {
      "path": "src/charts/area-chart.tsx",
      "content": "\"use client\";\n\nimport { ParentSize } from \"@visx/responsive\";\nimport type { Transition } from \"motion/react\";\nimport {\n  Children,\n  type CSSProperties,\n  isValidElement,\n  type ReactNode,\n  useCallback,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { Area, type AreaProps } from \"./area\";\nimport type { LineConfig, Margin } from \"./chart-context\";\nimport { ChartLoadingLabel } from \"./chart-loading-label\";\nimport {\n  type ChartPhase,\n  type ChartStatus,\n  DEFAULT_CHART_STATUS,\n  DEFAULT_Y_DOMAIN_TWEEN_MS,\n  resolveRestingChartPhase,\n} from \"./chart-phase\";\nimport { PatternArea } from \"./pattern-area\";\nimport { TimeSeriesChartInner } from \"./time-series-chart-shell\";\n\nexport interface AreaChartProps {\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  /** Motion enter transition (spring or cubic-bezier tween). */\n  enterTransition?: Transition;\n  /** Signature of motion URL state — triggers reveal replay when it changes. */\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  /** Loading vs ready — drives chart phase and loading chrome. Default: `\"ready\"`. */\n  status?: ChartStatus;\n  /** Centered shimmer label while loading. */\n  loadingLabel?: string;\n  /** Animate y-domain over this duration (ms) on status transitions. Default: 500. */\n  yDomainTweenDuration?: number;\n  /** Animate y-domain when status or target domain changes. Default: true */\n  yDomainTween?: boolean;\n  /** Visible x-domain for brush zoom. */\n  xDomain?: [Date, Date];\n  /** Full dataset length for x-scale padding when `xDomain` is set. */\n  xDomainSlotCount?: number;\n  /** Tween y-domain when brush changes the visible x-range. Default: false */\n  tweenYDomainOnXDomainChange?: boolean;\n  /** Inline container styles (e.g. fixed height for brush strip). */\n  style?: CSSProperties;\n  /** Fires when the internal chart phase changes (e.g. OG capture readiness). */\n  onPhaseChange?: (phase: ChartPhase) => void;\n  /** Child components (Area, Grid, ChartTooltip, etc.) */\n  children: ReactNode;\n}\n\nconst DEFAULT_MARGIN: Margin = { top: 40, right: 40, bottom: 40, left: 40 };\n\nfunction extractAreaConfigs(children: ReactNode): LineConfig[] {\n  const configs: LineConfig[] = [];\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 AreaProps | undefined;\n    const isPatternArea =\n      componentName === \"PatternArea\" || child.type === PatternArea;\n    const isAreaComponent =\n      componentName === \"Area\" ||\n      child.type === Area ||\n      (props &&\n        typeof props.dataKey === \"string\" &&\n        props.dataKey.length > 0 &&\n        !isPatternArea);\n\n    if (isAreaComponent && props?.dataKey) {\n      configs.push({\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  });\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  chartStatus: ChartStatus;\n  loadingLabel?: string;\n  yDomainTweenDuration: number;\n  yDomainTween: boolean;\n  xDomain?: [Date, Date];\n  xDomainSlotCount?: number;\n  tweenYDomainOnXDomainChange?: boolean;\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  chartStatus,\n  loadingLabel,\n  yDomainTweenDuration,\n  yDomainTween,\n  xDomain,\n  xDomainSlotCount,\n  tweenYDomainOnXDomainChange,\n  children,\n  containerRef,\n  onPhaseChange,\n}: ChartInnerProps) {\n  const lines = useMemo(() => extractAreaConfigs(children), [children]);\n\n  return (\n    <TimeSeriesChartInner\n      animationDuration={animationDuration}\n      animationEasing={animationEasing}\n      chartStatus={chartStatus}\n      clipPathId=\"chart-area-grow-clip\"\n      containerRef={containerRef}\n      data={data}\n      enterTransition={enterTransition}\n      height={height}\n      lines={lines}\n      loadingLabel={loadingLabel}\n      margin={margin}\n      onPhaseChange={onPhaseChange}\n      revealSignature={revealSignature}\n      tweenYDomainOnXDomainChange={tweenYDomainOnXDomainChange}\n      width={width}\n      xDataKey={xDataKey}\n      xDomain={xDomain}\n      xDomainSlotCount={xDomainSlotCount}\n      yDomainTween={yDomainTween}\n      yDomainTweenDuration={yDomainTweenDuration}\n    >\n      {children}\n    </TimeSeriesChartInner>\n  );\n}\n\nexport function AreaChart({\n  data,\n  xDataKey = \"date\",\n  margin: marginProp,\n  animationDuration = 1100,\n  animationEasing,\n  enterTransition,\n  revealSignature,\n  aspectRatio = \"2 / 1\",\n  className = \"\",\n  status = DEFAULT_CHART_STATUS,\n  loadingLabel,\n  yDomainTweenDuration = DEFAULT_Y_DOMAIN_TWEEN_MS,\n  yDomainTween = true,\n  xDomain,\n  xDomainSlotCount,\n  tweenYDomainOnXDomainChange = false,\n  style,\n  onPhaseChange,\n  children,\n}: AreaChartProps) {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const margin = { ...DEFAULT_MARGIN, ...marginProp };\n  const [chartPhase, setChartPhase] = useState<ChartPhase>(() =>\n    resolveRestingChartPhase(status)\n  );\n  const handlePhaseChange = useCallback(\n    (phase: ChartPhase) => {\n      setChartPhase(phase);\n      onPhaseChange?.(phase);\n    },\n    [onPhaseChange]\n  );\n\n  const showLoadingLabel = Boolean(\n    loadingLabel?.trim() &&\n      (chartPhase === \"loading\" ||\n        chartPhase === \"exiting\" ||\n        chartPhase === \"gridTweenReady\" ||\n        chartPhase === \"revealingLoading\")\n  );\n\n  return (\n    <div\n      className={cn(\"relative w-full\", className)}\n      ref={containerRef}\n      style={{ aspectRatio, touchAction: \"none\", ...style }}\n    >\n      <ParentSize debounceTime={10}>\n        {({ width, height }) => (\n          <ChartInner\n            animationDuration={animationDuration}\n            animationEasing={animationEasing}\n            chartStatus={status}\n            containerRef={containerRef}\n            data={data}\n            enterTransition={enterTransition}\n            height={height}\n            loadingLabel={loadingLabel}\n            margin={margin}\n            onPhaseChange={handlePhaseChange}\n            revealSignature={revealSignature}\n            tweenYDomainOnXDomainChange={tweenYDomainOnXDomainChange}\n            width={width}\n            xDataKey={xDataKey}\n            xDomain={xDomain}\n            xDomainSlotCount={xDomainSlotCount}\n            yDomainTween={yDomainTween}\n            yDomainTweenDuration={yDomainTweenDuration}\n          >\n            {children}\n          </ChartInner>\n        )}\n      </ParentSize>\n      {showLoadingLabel ? (\n        <ChartLoadingLabel\n          exiting={chartPhase !== \"loading\"}\n          text={loadingLabel}\n        />\n      ) : null}\n    </div>\n  );\n}\n\nexport { Area, type AreaProps } from \"./area\";\n\nexport default AreaChart;\n",
      "type": "registry:component",
      "target": "components/charts/area-chart.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/area.tsx",
      "content": "\"use client\";\n\nimport { curveMonotoneX } from \"@visx/curve\";\nimport { AreaClosed, LinePath } from \"@visx/shape\";\n\n// CurveFactory type - simplified version compatible with visx\n// biome-ignore lint/suspicious/noExplicitAny: d3 curve factory type\ntype CurveFactory = any;\n\nimport { useCallback, useId, useMemo, useRef, useState } from \"react\";\nimport { AreaGradientDefs } from \"./area-gradient-defs\";\nimport { chartCssVars, useChartStable, useYScale } from \"./chart-context\";\nimport type { ChartPhase, LoadingStyle } from \"./chart-phase\";\nimport { type FadeEdges, resolveFadeSides } from \"./fade-edges\";\nimport {\n  type LineLoadingPulseMode,\n  LineLoadingPulseStroke,\n  resolveLineLoadingPulseMode,\n} from \"./line-loading-pulse\";\nimport { LINE_LOADING_LOOP_PAUSE_MS } from \"./line-loading-timing\";\nimport { LineLoadingSweep } from \"./loading-sweep\";\nimport {\n  resolveDashTailBounds,\n  usePathStrokeMetrics,\n} from \"./path-stroke-utils\";\nimport { SeriesDashTailOverlay } from \"./series-dash-tail-overlay\";\nimport { SeriesHighlightLayer } from \"./series-highlight-layer\";\nimport { SeriesHoverDim } from \"./series-hover-dim\";\nimport { SeriesMarkers } from \"./series-markers\";\nimport type { SeriesPointMarkerStyle } from \"./series-point-marker\";\n\nexport interface AreaProps {\n  /** Key in data to use for y values */\n  dataKey: string;\n  /** Y-scale group id (Recharts `yAxisId`). Default: `\"left\"`. */\n  yAxisId?: string | number;\n  /** Fill color for the area gradient start. Default: var(--chart-line-primary) */\n  fill?: string;\n  /** Fill opacity at the top of the area. Default: 0.4 */\n  fillOpacity?: number;\n  /** Stroke color for the line. Default: same as fill */\n  stroke?: string;\n  /** Stroke width. Default: 2 */\n  strokeWidth?: number;\n  /** Curve function. Default: curveMonotoneX */\n  curve?: CurveFactory;\n  /** Whether to animate the area. Default: true */\n  animate?: boolean;\n  /** Whether to show the stroke line. Default: true */\n  showLine?: boolean;\n  /** Whether to show highlight segment on hover. Default: true */\n  showHighlight?: boolean;\n  /** Gradient opacity at bottom (0 = fully transparent). Default: 0 */\n  gradientToOpacity?: number;\n  /**\n   * Vertical extent of the fill gradient (0–1). `1` fades across the full\n   * height; lower values compress the gradient toward the top.\n   */\n  gradientSpan?: number;\n  /**\n   * Fade the area fill (and stroke) toward transparent at the chart edges.\n   * - `true` fades both edges, `false` disables the fade entirely.\n   * - `\"left\"` / `\"right\"` fades only that side — useful when the opposite\n   *   edge butts up against another element you don't want to fade into.\n   * Default: false\n   */\n  fadeEdges?: FadeEdges;\n  /** Render scatter-style circle markers at each data point. Default: false */\n  showMarkers?: boolean;\n  /** Marker styling (same options as Scatter). */\n  markers?: SeriesPointMarkerStyle;\n  /**\n   * Data index from which the line stroke becomes dashed (inclusive).\n   * Useful for projecting incomplete periods, e.g. dashed from yesterday through today.\n   */\n  dashFromIndex?: number;\n  /** Dash pattern for the tail segment when `dashFromIndex` is set. Default: \"6,4\" */\n  dashArray?: string;\n  /** Pulse stroke color while chart is loading. Default: var(--foreground) */\n  loadingStroke?: string;\n  /** Pulse stroke opacity while chart is loading. Default: 0.5 */\n  loadingStrokeOpacity?: number;\n  /**\n   * Show the loading pulse overlay. Default: follows chart loading phase.\n   * Set `false` to disable even during loading.\n   */\n  loading?: boolean;\n  /** Override pulse animation mode (loop / exit / enter). */\n  loadingPulseMode?: LineLoadingPulseMode;\n  /**\n   * Loading animation while the chart is in loading status: the default\n   * traveling `\"pulse\"`, or a diagonal `\"sweep\"` shimmer across the skeleton\n   * area. Default: `\"pulse\"`.\n   */\n  loadingStyle?: LoadingStyle;\n}\n\nfunction useAreaLoadingPulseState(\n  chartPhase: ChartPhase,\n  loading: boolean | undefined,\n  loadingPulseMode: LineLoadingPulseMode | undefined,\n  notifyLoadingPulseComplete?: () => void\n) {\n  const phasePulseMode = resolveLineLoadingPulseMode(chartPhase);\n  const pulseMode =\n    loading === false\n      ? null\n      : (loadingPulseMode ?? (loading === true ? \"loop\" : phasePulseMode));\n  const showLoadingPulse = pulseMode != null;\n  const showSeriesContent =\n    chartPhase === \"revealing\" ||\n    chartPhase === \"ready\" ||\n    chartPhase === \"exitingReady\";\n  const [pulseEpoch, setPulseEpoch] = useState(0);\n\n  const handleLoadingPulseComplete = useCallback(() => {\n    if (pulseMode === \"loop\") {\n      window.setTimeout(() => {\n        setPulseEpoch((epoch) => epoch + 1);\n      }, LINE_LOADING_LOOP_PAUSE_MS);\n      return;\n    }\n    notifyLoadingPulseComplete?.();\n  }, [notifyLoadingPulseComplete, pulseMode]);\n\n  return {\n    handleLoadingPulseComplete,\n    pulseMode,\n    pulseEpoch,\n    showLoadingPulse,\n    showSeriesContent,\n  };\n}\n\n// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: mirrors Line series layout (fill, stroke, dash, markers, pulse)\nexport function Area({\n  dataKey,\n  yAxisId,\n  fill = chartCssVars.linePrimary,\n  fillOpacity = 0.4,\n  stroke,\n  strokeWidth = 2,\n  curve = curveMonotoneX,\n  animate = true,\n  showLine = true,\n  showHighlight = true,\n  gradientToOpacity = 0,\n  gradientSpan = 1,\n  fadeEdges = false,\n  showMarkers = false,\n  markers,\n  dashFromIndex,\n  dashArray = \"6,4\",\n  loading,\n  loadingStroke = chartCssVars.foreground,\n  loadingStrokeOpacity = 0.5,\n  loadingPulseMode,\n  loadingStyle = \"pulse\",\n}: AreaProps) {\n  // Stable slice only: hover state lives inside `<SeriesHoverDim>` and\n  // `<SeriesHighlightLayer>` so this component (and its expensive\n  // <SeriesDashTailOverlay> child) does not re-render on cursor motion.\n  // The reveal-clip is now a single shared clipPath at the chart-shell\n  // level (`time-series-chart-shell.tsx`); we no longer render a per-area\n  // `<ChartRevealClip>` or read `revealEpoch` here.\n  const {\n    data,\n    renderData,\n    xScale,\n    innerHeight,\n    innerWidth,\n    xAccessor,\n    lines,\n    chartPhase,\n    notifyLoadingPulseComplete,\n  } = useChartStable();\n  const yScale = useYScale(yAxisId);\n  const {\n    handleLoadingPulseComplete,\n    pulseMode,\n    pulseEpoch,\n    showLoadingPulse,\n    showSeriesContent,\n  } = useAreaLoadingPulseState(\n    chartPhase,\n    loading,\n    loadingPulseMode,\n    notifyLoadingPulseComplete\n  );\n\n  const seriesIndex = useMemo(() => {\n    const index = lines.findIndex((line) => line.dataKey === dataKey);\n    return index >= 0 ? index : 0;\n  }, [lines, dataKey]);\n\n  const pathRef = useRef<SVGPathElement>(null);\n  const { pathLength, pathD } = usePathStrokeMetrics(pathRef, [\n    renderData,\n    innerWidth,\n    dashFromIndex,\n    showLine,\n    showSeriesContent,\n    showLoadingPulse,\n  ]);\n\n  // Unique IDs for this area\n  const uniqueId = useId();\n  const gradientId = `area-gradient-${dataKey}-${uniqueId}`;\n  const strokeGradientId = `area-stroke-gradient-${dataKey}-${uniqueId}`;\n  const edgeMaskId = `area-edge-mask-${dataKey}-${uniqueId}`;\n  const edgeGradientId = `${edgeMaskId}-gradient`;\n\n  const isPatternFill = fill.startsWith(\"url(\");\n  const showAreaFill = isPatternFill || fillOpacity > 0;\n  const areaFill = isPatternFill ? fill : `url(#${gradientId})`;\n\n  // Resolved stroke color (defaults to fill; pattern URLs need a real color)\n  const resolvedStroke =\n    stroke || (isPatternFill ? chartCssVars.linePrimary : fill);\n\n  const getY = useCallback(\n    (d: Record<string, unknown>) => {\n      const value = d[dataKey];\n      return typeof value === \"number\" ? (yScale(value) ?? 0) : 0;\n    },\n    [dataKey, yScale]\n  );\n\n  const hasDashTail = resolveDashTailBounds(dashFromIndex, data.length);\n  // The stroke gradient is only emitted when at least one edge fades, so fall\n  // back to the resolved solid color otherwise — avoids an invalid url(#...).\n  const fadeSides = resolveFadeSides(fadeEdges);\n  const useViewportEdgeFade = fadeSides.any && !isPatternFill;\n  let strokePaint = resolvedStroke;\n  if (!useViewportEdgeFade && fadeSides.any) {\n    strokePaint = `url(#${strokeGradientId})`;\n  }\n  const highlightEnabled =\n    showHighlight && showLine && !showLoadingPulse && showSeriesContent;\n  const showSeriesStroke = showSeriesContent && showLine;\n  let visibleStroke = \"transparent\";\n  if (showSeriesStroke && !hasDashTail) {\n    visibleStroke = strokePaint;\n  }\n  const shouldMeasurePath = showLine && (showSeriesContent || showLoadingPulse);\n\n  const seriesLayers = (\n    <>\n      {showSeriesContent && showAreaFill ? (\n        <AreaClosed\n          curve={curve}\n          data={renderData}\n          fill={areaFill}\n          x={(d) => xScale(xAccessor(d)) ?? 0}\n          y={getY}\n          yScale={yScale}\n        />\n      ) : null}\n\n      {shouldMeasurePath ? (\n        <>\n          <LinePath\n            curve={curve}\n            data={renderData}\n            innerRef={pathRef}\n            stroke={visibleStroke}\n            strokeLinecap=\"round\"\n            strokeWidth={strokeWidth}\n            x={(d) => xScale(xAccessor(d)) ?? 0}\n            y={getY}\n          />\n          {showSeriesStroke ? (\n            <SeriesDashTailOverlay\n              dashArray={dashArray}\n              dashFromIndex={dashFromIndex}\n              data={data}\n              innerHeight={innerHeight}\n              innerWidth={innerWidth}\n              pathD={pathD}\n              pathLength={pathLength}\n              stroke={strokePaint}\n              strokeWidth={strokeWidth}\n              xAccessor={xAccessor}\n              xScale={xScale}\n            />\n          ) : null}\n        </>\n      ) : null}\n    </>\n  );\n\n  // Sweep style owns all loading modes (loop + the exit/enter transitions),\n  // drawing its own silhouette; the pulse covers the default style.\n  const sweepLoading =\n    showLoadingPulse && innerWidth > 0 && loadingStyle === \"sweep\";\n  const pulseLoading = showLoadingPulse && innerWidth > 0 && !sweepLoading;\n\n  return (\n    <>\n      <AreaGradientDefs\n        edgeGradientId={edgeGradientId}\n        edgeMaskId={edgeMaskId}\n        fadeEdges={fadeEdges}\n        fill={fill}\n        fillOpacity={fillOpacity}\n        gradientId={gradientId}\n        gradientSpan={gradientSpan}\n        gradientToOpacity={gradientToOpacity}\n        innerHeight={innerHeight}\n        innerWidth={innerWidth}\n        isPatternFill={isPatternFill}\n        resolvedStroke={resolvedStroke}\n        strokeGradientId={strokeGradientId}\n      />\n\n      <SeriesHoverDim\n        dimOpacity={0.6}\n        enabled={showHighlight}\n        seriesIndex={seriesIndex}\n      >\n        {useViewportEdgeFade ? (\n          <g mask={`url(#${edgeMaskId})`}>{seriesLayers}</g>\n        ) : (\n          seriesLayers\n        )}\n      </SeriesHoverDim>\n\n      {/* Highlight segment on hover — isolated hover subscriber. */}\n      <SeriesHighlightLayer\n        enabled={highlightEnabled}\n        height={innerHeight}\n        pathRef={pathRef}\n        stroke={resolvedStroke}\n        strokeWidth={strokeWidth}\n      />\n\n      {showMarkers && showSeriesContent ? (\n        <SeriesMarkers\n          animate={animate}\n          dataKey={dataKey}\n          {...markers}\n          fill={markers?.fill ?? resolvedStroke}\n          stroke={markers?.stroke ?? markers?.fill ?? resolvedStroke}\n        />\n      ) : null}\n\n      {sweepLoading ? (\n        <LineLoadingSweep\n          curve={curve}\n          key=\"loading-sweep\"\n          mode={pulseMode ?? \"loop\"}\n          onTransitionComplete={handleLoadingPulseComplete}\n          stroke={loadingStroke}\n          strokeOpacity={loadingStrokeOpacity}\n          strokeWidth={strokeWidth}\n          withArea\n        />\n      ) : null}\n      {pulseLoading && pathD ? (\n        <LineLoadingPulseStroke\n          key=\"loading-pulse\"\n          loopEpoch={pulseEpoch}\n          mode={pulseMode ?? undefined}\n          onCycleComplete={handleLoadingPulseComplete}\n          pathD={pathD}\n          stroke={loadingStroke}\n          strokeOpacity={loadingStrokeOpacity}\n          strokeWidth={strokeWidth}\n        />\n      ) : null}\n    </>\n  );\n}\n\nArea.displayName = \"Area\";\n\nexport default Area;\n",
      "type": "registry:component",
      "target": "components/charts/area.tsx"
    },
    {
      "path": "src/charts/area-chart-loading.tsx",
      "content": "\"use client\";\n\nimport { curveNatural } from \"@visx/curve\";\nimport { useMemo } from \"react\";\nimport { Area } from \"./area\";\nimport { AreaChart } from \"./area-chart\";\nimport type { Margin } from \"./chart-context\";\nimport type { LoadingStyle } from \"./chart-phase\";\nimport {\n  DEFAULT_SKELETON_DATA_KEY,\n  DEFAULT_SKELETON_POINT_COUNT,\n  generateChartSkeletonData,\n} from \"./generate-chart-skeleton-data\";\nimport { Grid } from \"./grid\";\n\nconst LOADING_DATA_KEY = DEFAULT_SKELETON_DATA_KEY;\nconst DEFAULT_LOADING_STROKE = \"var(--foreground)\";\nconst DEFAULT_LOADING_GRID_STROKE =\n  \"color-mix(in oklch, var(--chart-grid) 50%, transparent)\";\nconst DEFAULT_LOADING_GRID_SHIMMER_STROKE =\n  \"color-mix(in oklch, var(--foreground) 68%, transparent)\";\nconst DEFAULT_LOADING_STROKE_OPACITY = 0.5;\n\nexport interface AreaChartLoadingProps {\n  /** Chart margins */\n  margin?: Partial<Margin>;\n  /** Stroke color for the animated loading segment. */\n  stroke?: string;\n  /** Stroke opacity for the animated loading segment. Default: 0.5 */\n  strokeOpacity?: number;\n  /** Grid line stroke (color and opacity via color-mix or oklch alpha). */\n  gridStroke?: string;\n  /** Shimmer band stroke (color and opacity via color-mix or oklch alpha). */\n  gridShimmerStroke?: string;\n  /** Animate a shimmer band across grid lines. Default: true */\n  gridShimmer?: boolean;\n  /** Shimmer band width in pixels. Default: 140 */\n  gridShimmerLength?: number;\n  /** Shimmer speed multiplier (higher = faster). Default: 1 */\n  gridShimmerSpeed?: number;\n  /** Match shimmer loop to the loading line pulse (cycle + inter-loop pause). */\n  gridShimmerSync?: boolean;\n  /** Loading animation: `\"pulse\"` (default traveling pulse) or `\"sweep\"` (a\n   * diagonal shimmer across the skeleton area). Default: `\"pulse\"`. */\n  loadingStyle?: LoadingStyle;\n  /** Centered shimmer label text. Default: \"Loading\" */\n  label?: string;\n  /** Aspect ratio as \"width / height\". Default: \"2 / 1\" */\n  aspectRatio?: string;\n  /** Additional class name for the container */\n  className?: string;\n}\n\nexport function AreaChartLoading({\n  margin,\n  stroke = DEFAULT_LOADING_STROKE,\n  strokeOpacity = DEFAULT_LOADING_STROKE_OPACITY,\n  gridStroke = DEFAULT_LOADING_GRID_STROKE,\n  gridShimmerStroke = DEFAULT_LOADING_GRID_SHIMMER_STROKE,\n  gridShimmer = true,\n  gridShimmerLength,\n  gridShimmerSpeed,\n  gridShimmerSync = false,\n  loadingStyle = \"pulse\",\n  label = \"Loading\",\n  aspectRatio = \"2 / 1\",\n  className = \"\",\n}: AreaChartLoadingProps) {\n  const data = useMemo(\n    () =>\n      generateChartSkeletonData({\n        dataKey: DEFAULT_SKELETON_DATA_KEY,\n        pointCount: DEFAULT_SKELETON_POINT_COUNT,\n      }),\n    []\n  );\n\n  return (\n    <AreaChart\n      animationDuration={0}\n      aspectRatio={aspectRatio}\n      className={className}\n      data={data}\n      loadingLabel={label}\n      margin={margin}\n      status=\"loading\"\n    >\n      <Grid\n        horizontal\n        shimmer={loadingStyle === \"sweep\" ? false : gridShimmer}\n        shimmerLength={gridShimmerLength}\n        shimmerSpeed={gridShimmerSpeed}\n        shimmerStroke={gridShimmerStroke}\n        shimmerSync={gridShimmerSync}\n        stroke={gridStroke}\n      />\n      <Area\n        curve={curveNatural}\n        dataKey={LOADING_DATA_KEY}\n        fadeEdges={false}\n        fill=\"transparent\"\n        fillOpacity={0}\n        loading\n        loadingStroke={stroke}\n        loadingStrokeOpacity={strokeOpacity}\n        loadingStyle={loadingStyle}\n        showHighlight={false}\n        showLine\n        stroke=\"transparent\"\n        strokeWidth={2}\n      />\n    </AreaChart>\n  );\n}\n\nexport default AreaChartLoading;\n",
      "type": "registry:component",
      "target": "components/charts/area-chart-loading.tsx"
    },
    {
      "path": "src/charts/line-loading-pulse.tsx",
      "content": "\"use client\";\n\nimport { animate, motion, useMotionValue, useTransform } from \"motion/react\";\nimport { useEffect, useId } from \"react\";\nimport { chartCssVars, useChartStable } from \"./chart-context\";\nimport type { ChartPhase } from \"./chart-phase\";\nimport {\n  fadeGradientStops,\n  resolveFadeSides,\n  viewportFadeGradientAttrs,\n} from \"./fade-edges\";\nimport {\n  LINE_LOADING_PULSE_CYCLE_S,\n  LINE_LOADING_PULSE_EASE,\n} from \"./line-loading-timing\";\n\nconst CLIP_PADDING = 10;\n\nexport type LineLoadingPulseMode = \"loop\" | \"exit\" | \"enter\";\n\nexport function resolveLineLoadingPulseMode(\n  phase: ChartPhase\n): LineLoadingPulseMode | null {\n  switch (phase) {\n    case \"loading\":\n      return \"loop\";\n    case \"exiting\":\n      return \"exit\";\n    case \"revealingLoading\":\n      return \"enter\";\n    default:\n      return null;\n  }\n}\n\nexport interface LineLoadingPulseStrokeProps {\n  pathD: string;\n  mode?: LineLoadingPulseMode;\n  /** Bumps to restart loop cycles without remounting the stroke. */\n  loopEpoch?: number;\n  stroke?: string;\n  /** Stroke opacity for the animated segment. Default: 0.5 */\n  strokeOpacity?: number;\n  strokeWidth?: number;\n  onCycleComplete?: () => void;\n}\n\nfunction useGrowExitClip(\n  innerWidth: number,\n  mode: LineLoadingPulseMode,\n  loopEpoch: number,\n  onComplete?: () => void\n) {\n  const progress = useMotionValue(0);\n  const paddedFullWidth = innerWidth + CLIP_PADDING * 2;\n  const rightEdge = innerWidth + CLIP_PADDING;\n\n  const clipWidth = useTransform(progress, (p) => {\n    if (p <= 0.5) {\n      return (p / 0.5) * paddedFullWidth;\n    }\n    const shrink = (p - 0.5) / 0.5;\n    return (1 - shrink) * paddedFullWidth;\n  });\n\n  const clipX = useTransform(progress, (p) => {\n    if (p <= 0.5) {\n      return -CLIP_PADDING;\n    }\n    const shrink = (p - 0.5) / 0.5;\n    return rightEdge - (1 - shrink) * paddedFullWidth;\n  });\n\n  // biome-ignore lint/correctness/useExhaustiveDependencies: loopEpoch restarts pulse when orchestrator advances\n  useEffect(() => {\n    if (innerWidth <= 0) {\n      return;\n    }\n\n    const halfCycleS = LINE_LOADING_PULSE_CYCLE_S / 2;\n    let cancelled = false;\n    let controls: ReturnType<typeof animate> | undefined;\n\n    const finish = () => {\n      if (!cancelled) {\n        onComplete?.();\n      }\n    };\n\n    const runShrink = (from: number) => {\n      const shrinkDuration = halfCycleS * ((1 - from) / 0.5);\n      controls = animate(progress, 1, {\n        duration: Math.max(shrinkDuration, 0.01),\n        ease: [...LINE_LOADING_PULSE_EASE],\n        onComplete: finish,\n      });\n    };\n\n    if (mode === \"loop\") {\n      progress.set(0);\n      controls = animate(progress, 1, {\n        duration: LINE_LOADING_PULSE_CYCLE_S,\n        ease: [...LINE_LOADING_PULSE_EASE],\n        onComplete: finish,\n      });\n      return () => {\n        cancelled = true;\n        controls?.stop();\n      };\n    }\n\n    if (mode === \"exit\") {\n      const current = progress.get();\n\n      if (current < 0.5) {\n        const growDuration = halfCycleS * ((0.5 - current) / 0.5);\n        controls = animate(progress, 0.5, {\n          duration: Math.max(growDuration, 0.01),\n          ease: [...LINE_LOADING_PULSE_EASE],\n          onComplete: () => {\n            if (!cancelled) {\n              runShrink(0.5);\n            }\n          },\n        });\n      } else {\n        runShrink(current);\n      }\n\n      return () => {\n        cancelled = true;\n        controls?.stop();\n      };\n    }\n\n    if (mode === \"enter\") {\n      progress.set(0);\n      controls = animate(progress, 0.5, {\n        duration: halfCycleS,\n        ease: [...LINE_LOADING_PULSE_EASE],\n        onComplete: finish,\n      });\n      return () => {\n        cancelled = true;\n        controls?.stop();\n      };\n    }\n  }, [innerWidth, loopEpoch, mode, onComplete, progress]);\n\n  return { clipX, clipWidth };\n}\n\nexport function LineLoadingPulseStroke({\n  pathD,\n  mode = \"loop\",\n  loopEpoch = 0,\n  stroke = chartCssVars.foreground,\n  strokeOpacity = 0.5,\n  strokeWidth = 2.5,\n  onCycleComplete,\n}: LineLoadingPulseStrokeProps) {\n  const { innerWidth, innerHeight } = useChartStable();\n  const reactId = useId();\n  const clipPathId = `line-loading-clip-${reactId}`;\n  const gradientId = `line-loading-gradient-${reactId}`;\n  const fadeStops = fadeGradientStops(resolveFadeSides(true));\n  const clipHeight = innerHeight + CLIP_PADDING * 2;\n  const { clipX, clipWidth } = useGrowExitClip(\n    innerWidth,\n    mode,\n    loopEpoch,\n    onCycleComplete\n  );\n\n  if (innerWidth <= 0) {\n    return null;\n  }\n\n  return (\n    <>\n      <defs>\n        <clipPath id={clipPathId}>\n          <motion.rect\n            height={clipHeight}\n            style={{ width: clipWidth, x: clipX }}\n            y={-CLIP_PADDING}\n          />\n        </clipPath>\n        <linearGradient\n          id={gradientId}\n          {...viewportFadeGradientAttrs(innerWidth)}\n        >\n          {fadeStops.map((stop) => (\n            <stop\n              key={stop.offset}\n              offset={stop.offset}\n              stopColor={stroke}\n              stopOpacity={stop.opacity}\n            />\n          ))}\n        </linearGradient>\n      </defs>\n      <path\n        clipPath={`url(#${clipPathId})`}\n        d={pathD}\n        fill=\"none\"\n        opacity={strokeOpacity}\n        stroke={`url(#${gradientId})`}\n        strokeLinecap=\"round\"\n        strokeWidth={strokeWidth}\n      />\n    </>\n  );\n}\n\nLineLoadingPulseStroke.displayName = \"LineLoadingPulseStroke\";\n\nexport default LineLoadingPulseStroke;\n",
      "type": "registry:component",
      "target": "components/charts/line-loading-pulse.tsx"
    },
    {
      "path": "src/charts/loading-sweep.tsx",
      "content": "\"use client\";\n\nimport { scaleLinear } from \"@visx/scale\";\nimport { AreaClosed, LinePath } from \"@visx/shape\";\nimport { motion, useReducedMotion } from \"motion/react\";\nimport {\n  useCallback,\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { chartCssVars, useChartStable } from \"./chart-context\";\nimport {\n  LINE_LOADING_PULSE_EASE,\n  LOADING_LABEL_EXIT_S,\n} from \"./line-loading-timing\";\n\n/**\n * Shared \"sweep\" loading visuals. A soft diagonal shimmer band travels across a\n * self-contained skeleton silhouette on a loop, painted via an SVG mask. The\n * silhouette re-randomizes between passes (held steady during a pass, re-rolled\n * once the band clears the right edge) so it reads as live and still loading,\n * without warping mid-sweep. Used as the `loadingStyle=\"sweep\"` alternative to\n * the traveling pulse on `<Line>` and `<Area>`, and as the skeleton for\n * `<BarChart status=\"loading\">`.\n */\n\n// CurveFactory type - simplified version compatible with visx\n// biome-ignore lint/suspicious/noExplicitAny: d3 curve factory type\ntype CurveFactory = any;\n\n/** One shimmer sweep, in seconds. */\nconst DEFAULT_SWEEP_DURATION_S = 2;\n/** Sweep travel in objectBoundingBox space: off the left edge to off the right. */\nconst SWEEP_START_X = -1;\nconst SWEEP_END_X = 2;\n/** Diagonal tilt of the shimmer band, in degrees. */\nconst SWEEP_ANGLE_DEG = 25;\nconst HEIGHT_MIN_PCT = 20;\nconst HEIGHT_MAX_PCT = 80;\nconst DEFAULT_POINT_COUNT = 14;\nconst BAR_CORNER_RADIUS = 2;\nconst DEFAULT_BAR_COUNT = 12;\nconst DEFAULT_FILL = \"var(--foreground)\";\nconst DEFAULT_BAR_FILL_OPACITY = 0.45;\nconst LINE_STROKE_OPACITY = 0.55;\nconst AREA_FILL_TOP_OPACITY = 0.18;\nconst AREA_FILL_BOTTOM_OPACITY = 0.02;\n/** Bar width as a fraction of its band (the rest is the inter-bar gap). */\nconst DEFAULT_BAR_FRACTION = 0.7;\n\n// ─── Pure, SSR-safe helpers ──────────────────────────────────────────────\n// Heights come from a deterministic hash of (index, seed), never\n// `Math.random()`, so the first server render and first client render agree\n// (no Next.js hydration mismatch). Re-randomizing only bumps the numeric seed\n// on the client, after a sweep completes.\n\n/** Cheap deterministic hash to a fractional part in [0, 1). */\nfunction hashFract(n: number): number {\n  const x = Math.sin(n) * 43_758.5453;\n  return x - Math.floor(x);\n}\n\n/** Deterministic heights (percentages of the available height) for a seed. */\nexport function getSkeletonHeights(\n  count: number,\n  seed = 0,\n  min = HEIGHT_MIN_PCT,\n  max = HEIGHT_MAX_PCT\n): number[] {\n  const range = max - min;\n  return Array.from(\n    { length: count },\n    (_, i) => min + Math.floor(hashFract((i + 1) * 12.9898 + seed) * range)\n  );\n}\n\n/** Deterministic up/down (±1) per bar for the \"center\" baseline. */\nfunction getSkeletonSigns(count: number, seed = 0): number[] {\n  return Array.from({ length: count }, (_, i) =>\n    hashFract((i + 1) * 78.233 + seed) < 0.5 ? -1 : 1\n  );\n}\n\n/** Bell-curve opacity stops (sin squared) for the shimmer band's soft edges. */\nfunction generateEasedGradientStops(\n  steps = 17,\n  minOpacity = 0.05,\n  maxOpacity = 0.9\n) {\n  return Array.from({ length: steps }, (_, i) => {\n    const t = i / (steps - 1);\n    const eased = Math.sin(t * Math.PI) ** 2;\n    const opacity = minOpacity + eased * (maxOpacity - minOpacity);\n    return {\n      offset: `${(t * 100).toFixed(0)}%`,\n      opacity: Number(opacity.toFixed(3)),\n    };\n  });\n}\n\n// ─── Shared mask defs (`${chartId}-mask` is the mask id) ─────────────────────\n\nfunction LoadingSweepMask({\n  chartId,\n  width,\n  height,\n  durationSeconds,\n  onSweepComplete,\n}: {\n  chartId: string;\n  width: number;\n  height: number;\n  durationSeconds: number;\n  onSweepComplete: () => void;\n}) {\n  const gradientStops = useMemo(() => generateEasedGradientStops(), []);\n  const lastXRef = useRef(SWEEP_START_X);\n\n  const handleUpdate = useCallback(\n    (latest: { x?: number }) => {\n      const xValue = typeof latest.x === \"number\" ? latest.x : SWEEP_START_X;\n      // Re-roll once the band has cleared the visible area (crossed past 1),\n      // so the silhouette never changes shape under the user's eye.\n      if (xValue >= 1 && lastXRef.current < 1) {\n        onSweepComplete();\n      }\n      lastXRef.current = xValue;\n    },\n    [onSweepComplete]\n  );\n\n  return (\n    <>\n      <linearGradient id={`${chartId}-grad`} x1=\"0\" x2=\"1\" y1=\"0\" y2=\"0\">\n        {gradientStops.map(({ offset, opacity }) => (\n          <stop\n            key={offset}\n            offset={offset}\n            stopColor=\"white\"\n            stopOpacity={opacity}\n          />\n        ))}\n      </linearGradient>\n      <pattern\n        height=\"1\"\n        id={`${chartId}-pattern`}\n        patternContentUnits=\"objectBoundingBox\"\n        patternTransform={`rotate(${SWEEP_ANGLE_DEG})`}\n        patternUnits=\"objectBoundingBox\"\n        width={3}\n        x=\"0\"\n        y=\"0\"\n      >\n        <motion.rect\n          animate={{ x: SWEEP_END_X }}\n          fill={`url(#${chartId}-grad)`}\n          height=\"1\"\n          initial={{ x: SWEEP_START_X }}\n          onUpdate={handleUpdate}\n          transition={{\n            duration: durationSeconds,\n            ease: \"linear\",\n            repeat: Number.POSITIVE_INFINITY,\n            repeatType: \"loop\",\n          }}\n          width=\"1\"\n          y=\"0\"\n        />\n      </pattern>\n      <mask id={`${chartId}-mask`} maskUnits=\"userSpaceOnUse\">\n        <rect fill={`url(#${chartId}-pattern)`} height={height} width={width} />\n      </mask>\n    </>\n  );\n}\n\n// ─── Line / Area loading sweep (its own re-randomizing silhouette) ───────────\n\nexport interface LineLoadingSweepProps {\n  /** Curve factory from the host `<Line>` / `<Area>`, so the silhouette matches\n   * the chart's interpolation (step, smooth, linear, …). */\n  curve: CurveFactory;\n  /** Fill the silhouette as an area (for `<Area>`); otherwise stroke only. */\n  withArea?: boolean;\n  /** Loading phase: `\"loop\"` (steady), `\"exit\"` (loading → ready), or `\"enter\"`\n   * (ready → loading). Exit/enter fade the silhouette and then signal the chart\n   * to continue its reveal. Default: `\"loop\"`. */\n  mode?: \"loop\" | \"exit\" | \"enter\";\n  /** Fired when an exit/enter transition finishes, to advance the chart phase. */\n  onTransitionComplete?: () => void;\n  stroke?: string;\n  strokeOpacity?: number;\n  strokeWidth?: number;\n  pointCount?: number;\n  durationSeconds?: number;\n}\n\n/**\n * Renders a placeholder line/area silhouette (its own, not the chart's skeleton)\n * with the shimmer sweeping across it. The silhouette re-randomizes between\n * passes. Reads inner dimensions from chart context.\n */\nexport function LineLoadingSweep({\n  curve,\n  withArea = false,\n  mode = \"loop\",\n  onTransitionComplete,\n  stroke = chartCssVars.foreground,\n  strokeOpacity = LINE_STROKE_OPACITY,\n  strokeWidth = 2,\n  pointCount = DEFAULT_POINT_COUNT,\n  durationSeconds = DEFAULT_SWEEP_DURATION_S,\n}: LineLoadingSweepProps) {\n  const { innerWidth, innerHeight } = useChartStable();\n  const reduceMotion = useReducedMotion();\n  const reactId = useId();\n  const chartId = `line-sweep-${reactId.replace(/[^a-zA-Z0-9_-]/g, \"\")}`;\n  const isLoop = mode === \"loop\";\n\n  const [tick, setTick] = useState(0);\n  // Re-randomize only while looping; hold the silhouette steady through a\n  // transition so it fades out (or in) as one piece.\n  const onSweepComplete = useCallback(() => {\n    if (isLoop) {\n      setTick((prev) => prev + 1);\n    }\n  }, [isLoop]);\n  const heights = useMemo(\n    () => getSkeletonHeights(pointCount, tick),\n    [pointCount, tick]\n  );\n\n  // With reduced motion there is no fade to await, so signal the handoff\n  // immediately or the phase machine would stall mid-transition.\n  useEffect(() => {\n    if (reduceMotion && !isLoop) {\n      onTransitionComplete?.();\n    }\n  }, [reduceMotion, isLoop, onTransitionComplete]);\n\n  if (innerWidth <= 0 || innerHeight <= 0 || heights.length < 2) {\n    return null;\n  }\n\n  const xScale = scaleLinear({\n    domain: [0, heights.length - 1],\n    range: [0, innerWidth],\n  });\n  const yScale = scaleLinear({ domain: [0, 100], range: [innerHeight, 0] });\n  const points = heights.map((value, index) => ({ index, value }));\n  const getX = (d: { index: number }) => xScale(d.index);\n  const getY = (d: { value: number }) => yScale(d.value);\n\n  const silhouette = (\n    <>\n      {withArea ? (\n        <AreaClosed\n          curve={curve}\n          data={points}\n          fill={`url(#${chartId}-area)`}\n          x={getX}\n          y={getY}\n          yScale={yScale}\n        />\n      ) : null}\n      <LinePath\n        curve={curve}\n        data={points}\n        fill=\"none\"\n        stroke={stroke}\n        strokeLinecap=\"round\"\n        strokeOpacity={strokeOpacity}\n        strokeWidth={strokeWidth}\n        x={getX}\n        y={getY}\n      />\n    </>\n  );\n\n  const areaGradient = withArea ? (\n    <linearGradient id={`${chartId}-area`} x1=\"0\" x2=\"0\" y1=\"0\" y2=\"1\">\n      <stop\n        offset=\"0%\"\n        stopColor={stroke}\n        stopOpacity={AREA_FILL_TOP_OPACITY}\n      />\n      <stop\n        offset=\"100%\"\n        stopColor={stroke}\n        stopOpacity={AREA_FILL_BOTTOM_OPACITY}\n      />\n    </linearGradient>\n  ) : null;\n\n  if (reduceMotion) {\n    return (\n      <>\n        {areaGradient ? <defs>{areaGradient}</defs> : null}\n        {silhouette}\n      </>\n    );\n  }\n\n  const maskUrl = `url(#${chartId}-mask)`;\n  const defs = (\n    <defs>\n      {areaGradient}\n      <LoadingSweepMask\n        chartId={chartId}\n        durationSeconds={durationSeconds}\n        height={innerHeight}\n        onSweepComplete={onSweepComplete}\n        width={innerWidth}\n      />\n    </defs>\n  );\n\n  if (isLoop) {\n    return (\n      <>\n        {defs}\n        <g mask={maskUrl}>{silhouette}</g>\n      </>\n    );\n  }\n\n  // Transition: fade the swept silhouette out (exit) or in (enter), then hand\n  // off to the chart so it can reveal the real series.\n  return (\n    <>\n      {defs}\n      <motion.g\n        animate={{ opacity: mode === \"exit\" ? 0 : 1 }}\n        initial={{ opacity: mode === \"exit\" ? 1 : 0 }}\n        mask={maskUrl}\n        onAnimationComplete={onTransitionComplete}\n        transition={{\n          duration: LOADING_LABEL_EXIT_S,\n          ease: [...LINE_LOADING_PULSE_EASE],\n        }}\n      >\n        {silhouette}\n      </motion.g>\n    </>\n  );\n}\n\nLineLoadingSweep.displayName = \"LineLoadingSweep\";\n\n// ─── Bar loading skeleton (seeded bars under the sweep, inner coords) ─────────\n\nfunction SkeletonBars({\n  heights,\n  signs,\n  innerWidth,\n  innerHeight,\n  baseline,\n  barFraction,\n  fill,\n  fillOpacity,\n}: {\n  heights: number[];\n  signs: number[];\n  innerWidth: number;\n  innerHeight: number;\n  baseline: \"bottom\" | \"center\";\n  barFraction: number;\n  fill: string;\n  fillOpacity: number;\n}) {\n  const bandWidth = innerWidth / heights.length;\n  const barW = bandWidth * barFraction;\n  const xOffset = (bandWidth * (1 - barFraction)) / 2;\n  const isCenter = baseline === \"center\";\n  const baselineY = isCenter ? innerHeight / 2 : innerHeight;\n  const halfBarH = isCenter ? innerHeight / 2 : innerHeight;\n\n  return (\n    <>\n      {heights.map((value, i) => {\n        const sign = isCenter ? (signs[i] ?? 1) : 1;\n        const barH = Math.max(1, halfBarH * (value / 100));\n        const x = i * bandWidth + xOffset;\n        const y = sign === 1 ? baselineY - barH : baselineY;\n        return (\n          <rect\n            fill={fill}\n            fillOpacity={fillOpacity}\n            height={barH}\n            key={`${x.toFixed(2)}-${value}`}\n            rx={BAR_CORNER_RADIUS}\n            width={barW}\n            x={x}\n            y={y}\n          />\n        );\n      })}\n    </>\n  );\n}\n\nexport interface BarLoadingSkeletonProps {\n  innerWidth: number;\n  innerHeight: number;\n  /** Number of skeleton bars. Default: 12 */\n  barCount?: number;\n  /** Bar fill color. Default: `var(--foreground)` */\n  fill?: string;\n  /** Bar fill opacity. Default: 0.45 */\n  fillOpacity?: number;\n  /** Bars rise from the bottom or diverge from the vertical center. Default: `\"bottom\"` */\n  baseline?: \"bottom\" | \"center\";\n  /** Bar width as a fraction of its band (0–1). Default: 0.7 */\n  barFraction?: number;\n  /** One shimmer sweep, in seconds. Default: 2 */\n  durationSeconds?: number;\n}\n\n/**\n * Skeleton bars masked by the shimmer sweep, re-randomizing between passes.\n * Rendered in the chart's inner coordinate space (origin at the inner top-left),\n * so a `BarChart` drops it inside its margin-translated group.\n */\nexport function BarLoadingSkeleton({\n  innerWidth,\n  innerHeight,\n  barCount = DEFAULT_BAR_COUNT,\n  fill = DEFAULT_FILL,\n  fillOpacity = DEFAULT_BAR_FILL_OPACITY,\n  baseline = \"bottom\",\n  barFraction = DEFAULT_BAR_FRACTION,\n  durationSeconds = DEFAULT_SWEEP_DURATION_S,\n}: BarLoadingSkeletonProps) {\n  const reduceMotion = useReducedMotion();\n  const reactId = useId();\n  const chartId = `bar-sweep-${reactId.replace(/[^a-zA-Z0-9_-]/g, \"\")}`;\n  const [tick, setTick] = useState(0);\n  const onSweepComplete = useCallback(() => setTick((prev) => prev + 1), []);\n  const heights = useMemo(\n    () => getSkeletonHeights(barCount, tick),\n    [barCount, tick]\n  );\n  const signs = useMemo(\n    () => getSkeletonSigns(barCount, tick),\n    [barCount, tick]\n  );\n\n  if (innerWidth <= 0 || innerHeight <= 0) {\n    return null;\n  }\n\n  const bars = (\n    <SkeletonBars\n      barFraction={barFraction}\n      baseline={baseline}\n      fill={fill}\n      fillOpacity={fillOpacity}\n      heights={heights}\n      innerHeight={innerHeight}\n      innerWidth={innerWidth}\n      signs={signs}\n    />\n  );\n\n  if (reduceMotion) {\n    return bars;\n  }\n\n  return (\n    <>\n      <defs>\n        <LoadingSweepMask\n          chartId={chartId}\n          durationSeconds={durationSeconds}\n          height={innerHeight}\n          onSweepComplete={onSweepComplete}\n          width={innerWidth}\n        />\n      </defs>\n      <g mask={`url(#${chartId}-mask)`}>{bars}</g>\n    </>\n  );\n}\n\nBarLoadingSkeleton.displayName = \"BarLoadingSkeleton\";\n",
      "type": "registry:component",
      "target": "components/charts/loading-sweep.tsx"
    },
    {
      "path": "src/charts/line-loading-timing.ts",
      "content": "/** Grow + exit timeline for `LineLoadingPulse` (seconds). */\nexport const LINE_LOADING_PULSE_CYCLE_S = 2.2;\n\n/** Idle gap before the loading line pulse restarts (milliseconds). */\nexport const LINE_LOADING_LOOP_PAUSE_MS = 280;\n\n/** Loading label exit on loading → ready (seconds). */\nexport const LOADING_LABEL_EXIT_S = 0.45;\n\n/** Loading label drops this many pixels while exiting. */\nexport const LOADING_LABEL_EXIT_Y_PX = 30;\n\nexport const LINE_LOADING_PULSE_EASE = [0.85, 0, 0.15, 1] as const;\n",
      "type": "registry:component",
      "target": "components/charts/line-loading-timing.ts"
    },
    {
      "path": "src/charts/chart-loading-label.tsx",
      "content": "\"use client\";\n\nimport { motion } from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\nimport { ShimmeringText } from \"../components/shimmering-text\";\nimport {\n  LINE_LOADING_PULSE_EASE,\n  LOADING_LABEL_EXIT_S,\n  LOADING_LABEL_EXIT_Y_PX,\n} from \"./line-loading-timing\";\n\nexport interface ChartLoadingLabelProps {\n  /** Label shown centered over the chart. */\n  text?: string;\n  className?: string;\n  /** Animate down, fade, and blur during loading → ready handoff. */\n  exiting?: boolean;\n}\n\nexport function ChartLoadingLabel({\n  text = \"Loading\",\n  className,\n  exiting = false,\n}: ChartLoadingLabelProps) {\n  if (!text.trim()) {\n    return null;\n  }\n\n  return (\n    <motion.div\n      animate={{\n        y: exiting ? LOADING_LABEL_EXIT_Y_PX : 0,\n        opacity: exiting ? 0 : 1,\n        filter: exiting ? \"blur(2px)\" : \"blur(0px)\",\n      }}\n      aria-live=\"polite\"\n      className={cn(\n        \"pointer-events-none absolute inset-0 flex items-center justify-center\",\n        className\n      )}\n      initial={false}\n      role=\"status\"\n      transition={{\n        duration: LOADING_LABEL_EXIT_S,\n        ease: [...LINE_LOADING_PULSE_EASE],\n      }}\n    >\n      <ShimmeringText\n        className=\"font-medium text-sm tracking-wide [--color:var(--muted-foreground)] [--shimmering-color:var(--foreground)]\"\n        text={text}\n      />\n    </motion.div>\n  );\n}\n\nexport default ChartLoadingLabel;\n",
      "type": "registry:component",
      "target": "components/charts/chart-loading-label.tsx"
    },
    {
      "path": "src/charts/pattern-area.tsx",
      "content": "\"use client\";\n\nimport { curveMonotoneX } from \"@visx/curve\";\nimport { AreaClosed } from \"@visx/shape\";\nimport { useChartStable } from \"./chart-context\";\n\n// biome-ignore lint/suspicious/noExplicitAny: d3 curve factory type\ntype CurveFactory = any;\n\nexport interface PatternAreaProps {\n  /** Key in data to use for y values */\n  dataKey: string;\n  /** Fill color or pattern URL (e.g. `url(#pattern-id)`) */\n  fill: string;\n  /** Curve function. Default: curveMonotoneX */\n  curve?: CurveFactory;\n  /** @deprecated Pattern fill is not clip-revealed; only the stroke `Area` animates. */\n  animate?: boolean;\n}\n\n/**\n * Filled area using an SVG pattern (`url(#id)`).\n * Pair with `PatternLines` in `AreaChart` children and an `Area` with `fillOpacity={0}` for the stroke line.\n */\nexport function PatternArea({\n  dataKey,\n  fill,\n  curve = curveMonotoneX,\n}: PatternAreaProps) {\n  const { renderData, xScale, yScale, xAccessor } = useChartStable();\n\n  return (\n    <AreaClosed\n      curve={curve}\n      data={renderData}\n      fill={fill}\n      x={(d) => xScale(xAccessor(d)) ?? 0}\n      y={(d) => {\n        const v = d[dataKey];\n        return typeof v === \"number\" ? (yScale(v) ?? 0) : 0;\n      }}\n      yScale={yScale}\n    />\n  );\n}\n\nPatternArea.displayName = \"PatternArea\";\n\nexport default PatternArea;\n",
      "type": "registry:component",
      "target": "components/charts/pattern-area.tsx"
    }
  ]
}