{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "chart-series",
  "type": "registry:component",
  "title": "Chart Series Layer",
  "description": "Shared line and area series rendering helpers (paths, markers, highlights, dash tails)",
  "dependencies": [
    "@visx/shape@4.0.1-alpha.0",
    "d3-shape",
    "motion"
  ],
  "registryDependencies": [
    "@bklit/chart-context",
    "@bklit/chart-animation",
    "@bklit/chart-tooltip"
  ],
  "files": [
    {
      "path": "src/charts/path-stroke-utils.ts",
      "content": "// biome-ignore-all lint/correctness/useExhaustiveDependencies: usePathStrokeMetrics intentionally accepts caller-controlled deps\nimport { type RefObject, useEffect, useState } from \"react\";\n\nexport function findPathLengthAtX(\n  path: SVGPathElement | null,\n  pathLength: number,\n  targetX: number\n): number {\n  if (!path || pathLength === 0) {\n    return 0;\n  }\n  let low = 0;\n  let high = pathLength;\n  const tolerance = 0.5;\n\n  while (high - low > tolerance) {\n    const mid = (low + high) / 2;\n    const point = path.getPointAtLength(mid);\n    if (point.x < targetX) {\n      low = mid;\n    } else {\n      high = mid;\n    }\n  }\n  return (low + high) / 2;\n}\n\ninterface PathStrokeMetrics {\n  pathD: string | null;\n  pathLength: number;\n}\n\nconst EMPTY_METRICS: PathStrokeMetrics = { pathD: null, pathLength: 0 };\n\n/**\n * Caller passes the references that drive the rendered path (renderData,\n * innerWidth, etc.) as `deps`. A stringified summary like\n * `${renderData.length}:${innerWidth}` is *not* safe here — same-length\n * in-place mutations of `renderData` keep the summary identical, so the\n * effect would never re-fire and `pathD`/`pathLength` would stay frozen on\n * the previous geometry (the area fill repaints from `renderData` directly\n * and would diverge from the stroke).\n */\nexport function usePathStrokeMetrics(\n  pathRef: RefObject<SVGPathElement | null>,\n  deps: readonly unknown[]\n): PathStrokeMetrics {\n  const [metrics, setMetrics] = useState<PathStrokeMetrics>(EMPTY_METRICS);\n\n  useEffect(() => {\n    const path = pathRef.current;\n    if (!path) {\n      return;\n    }\n    const d = path.getAttribute(\"d\");\n    const len = d ? path.getTotalLength() : 0;\n    setMetrics((prev) =>\n      prev.pathD === d && prev.pathLength === len\n        ? prev\n        : { pathD: d, pathLength: len }\n    );\n  }, deps);\n\n  return metrics;\n}\n\nexport function resolveDashTailBounds(\n  dashFromIndex: number | undefined,\n  dataLength: number\n): boolean {\n  return (\n    dashFromIndex != null &&\n    dashFromIndex >= 0 &&\n    dashFromIndex < dataLength - 1\n  );\n}\n\nexport function resolveDashStartX(\n  data: Record<string, unknown>[],\n  dashFromIndex: number,\n  xScale: (value: Date | number) => number | undefined,\n  xAccessor: (datum: Record<string, unknown>) => Date | number\n): number {\n  const dashFromPoint = data[dashFromIndex];\n  if (!dashFromPoint) {\n    return 0;\n  }\n  return xScale(xAccessor(dashFromPoint)) ?? 0;\n}\n",
      "type": "registry:component",
      "target": "components/charts/path-stroke-utils.ts"
    },
    {
      "path": "src/charts/series-path-utils.ts",
      "content": "import { line as d3Line } from \"d3-shape\";\n\n// biome-ignore lint/suspicious/noExplicitAny: d3 curve factory type\ntype CurveFactory = any;\n\nexport interface SeriesPathPoint {\n  x: number;\n  y: number;\n  key: string;\n}\n\nexport function computeSeriesPathPoints(\n  data: Record<string, unknown>[],\n  xAccessor: (datum: Record<string, unknown>) => Date,\n  xScale: (value: Date) => number | undefined,\n  yScale: (value: number) => number | undefined,\n  dataKey: string\n): SeriesPathPoint[] {\n  return data.map((datum, index) => {\n    const xValue = xAccessor(datum);\n    const yValue = datum[dataKey];\n    return {\n      x: xScale(xValue) ?? 0,\n      y: typeof yValue === \"number\" ? (yScale(yValue) ?? 0) : 0,\n      key: String(xValue.getTime?.() ?? index),\n    };\n  });\n}\n\nexport function interpolateSeriesPathPoints(\n  from: SeriesPathPoint[],\n  to: SeriesPathPoint[],\n  progress: number\n): SeriesPathPoint[] {\n  if (progress >= 1) {\n    return to;\n  }\n  if (progress <= 0) {\n    return from.length > 0 ? from : to;\n  }\n\n  const fromByKey = new Map(from.map((point) => [point.key, point]));\n\n  return to.map((target, index) => {\n    const source = fromByKey.get(target.key);\n    if (source) {\n      return {\n        key: target.key,\n        x: source.x + (target.x - source.x) * progress,\n        y: source.y + (target.y - source.y) * progress,\n      };\n    }\n\n    const previousTarget = index > 0 ? to[index - 1] : undefined;\n    const previousSource = previousTarget\n      ? fromByKey.get(previousTarget.key)\n      : undefined;\n    const nextTarget = index < to.length - 1 ? to[index + 1] : undefined;\n    const nextSource = nextTarget ? fromByKey.get(nextTarget.key) : undefined;\n    const anchor = previousSource ?? nextSource ?? from[0] ?? target;\n\n    return {\n      key: target.key,\n      x: anchor.x + (target.x - anchor.x) * progress,\n      y: anchor.y + (target.y - anchor.y) * progress,\n    };\n  });\n}\n\nexport function seriesPathFromPoints(\n  points: SeriesPathPoint[],\n  curve: CurveFactory\n): string {\n  if (points.length === 0) {\n    return \"\";\n  }\n\n  const generator = d3Line<SeriesPathPoint>()\n    .x((point) => point.x)\n    .y((point) => point.y)\n    .curve(curve);\n\n  return generator(points) ?? \"\";\n}\n\nexport function seriesPathTransitionSignature({\n  renderData,\n  xAccessor,\n  dataKey,\n  innerWidth,\n  xDomainMin,\n  xDomainMax,\n}: {\n  renderData: Record<string, unknown>[];\n  xAccessor: (datum: Record<string, unknown>) => Date;\n  dataKey: string;\n  innerWidth: number;\n  xDomainMin: number;\n  xDomainMax: number;\n}): string {\n  const values = renderData.map((datum) => {\n    const xValue = xAccessor(datum);\n    const yValue = datum[dataKey];\n    return `${xValue.getTime()}:${typeof yValue === \"number\" ? yValue : \"\"}`;\n  });\n\n  return `${innerWidth}|${xDomainMin}|${xDomainMax}|${values.join(\",\")}`;\n}\n",
      "type": "registry:component",
      "target": "components/charts/series-path-utils.ts"
    },
    {
      "path": "src/charts/use-animated-series-path.ts",
      "content": "\"use client\";\n\nimport { animate, useReducedMotion } from \"motion/react\";\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport { LINE_LOADING_PULSE_EASE } from \"./line-loading-timing\";\nimport {\n  computeSeriesPathPoints,\n  interpolateSeriesPathPoints,\n  type SeriesPathPoint,\n  seriesPathFromPoints,\n  seriesPathTransitionSignature,\n} from \"./series-path-utils\";\n\n// biome-ignore lint/suspicious/noExplicitAny: d3 curve factory type\ntype CurveFactory = any;\n\nexport interface UseAnimatedSeriesPathOptions {\n  renderData: Record<string, unknown>[];\n  xAccessor: (datum: Record<string, unknown>) => Date;\n  xScale: (value: Date) => number | undefined;\n  yScale: (value: number) => number | undefined;\n  dataKey: string;\n  curve: CurveFactory;\n  chartPhase: string;\n  durationMs: number;\n  innerWidth: number;\n  enabled: boolean;\n}\n\nexport function useAnimatedSeriesPath({\n  renderData,\n  xAccessor,\n  xScale,\n  yScale,\n  dataKey,\n  curve,\n  chartPhase,\n  durationMs,\n  innerWidth,\n  enabled,\n}: UseAnimatedSeriesPathOptions) {\n  const reducedMotion = useReducedMotion();\n  const [animatedPoints, setAnimatedPoints] = useState<\n    SeriesPathPoint[] | null\n  >(null);\n  const displayedPointsRef = useRef<SeriesPathPoint[] | null>(null);\n  const animatingRef = useRef(false);\n\n  const xScaleDomain = useMemo(() => {\n    const scaleWithDomain = xScale as { domain?: () => [Date, Date] };\n    return scaleWithDomain.domain?.() ?? [new Date(0), new Date(0)];\n  }, [xScale]);\n\n  const transitionSignature = useMemo(\n    () =>\n      seriesPathTransitionSignature({\n        renderData,\n        xAccessor,\n        dataKey,\n        innerWidth,\n        xDomainMin: xScaleDomain[0]?.getTime?.() ?? 0,\n        xDomainMax: xScaleDomain[1]?.getTime?.() ?? 0,\n      }),\n    [renderData, xAccessor, dataKey, innerWidth, xScaleDomain]\n  );\n\n  const targetPoints = useMemo(\n    () =>\n      computeSeriesPathPoints(renderData, xAccessor, xScale, yScale, dataKey),\n    [renderData, xAccessor, xScale, yScale, dataKey]\n  );\n\n  const prevTransitionSignatureRef = useRef(transitionSignature);\n\n  useEffect(() => {\n    if (!animatingRef.current) {\n      displayedPointsRef.current = targetPoints;\n    }\n  }, [targetPoints]);\n\n  useEffect(() => {\n    const shouldAnimate =\n      enabled &&\n      !reducedMotion &&\n      chartPhase === \"ready\" &&\n      durationMs > 0 &&\n      renderData.length > 0;\n\n    if (!shouldAnimate) {\n      animatingRef.current = false;\n      setAnimatedPoints(null);\n      displayedPointsRef.current = targetPoints;\n      prevTransitionSignatureRef.current = transitionSignature;\n      return;\n    }\n\n    if (prevTransitionSignatureRef.current === transitionSignature) {\n      return;\n    }\n    prevTransitionSignatureRef.current = transitionSignature;\n\n    const fromPoints = displayedPointsRef.current ?? targetPoints;\n    if (fromPoints.length === 0) {\n      displayedPointsRef.current = targetPoints;\n      return;\n    }\n\n    animatingRef.current = true;\n    const fromSnapshot = fromPoints;\n\n    const control = animate(0, 1, {\n      duration: durationMs / 1000,\n      ease: [...LINE_LOADING_PULSE_EASE],\n      onUpdate: (progress) => {\n        const currentTarget = computeSeriesPathPoints(\n          renderData,\n          xAccessor,\n          xScale,\n          yScale,\n          dataKey\n        );\n        const next = interpolateSeriesPathPoints(\n          fromSnapshot,\n          currentTarget,\n          progress\n        );\n        displayedPointsRef.current = next;\n        setAnimatedPoints(next);\n      },\n      onComplete: () => {\n        animatingRef.current = false;\n        displayedPointsRef.current = targetPoints;\n        setAnimatedPoints(null);\n      },\n    });\n\n    return () => {\n      control.stop();\n      animatingRef.current = false;\n    };\n  }, [\n    transitionSignature,\n    chartPhase,\n    durationMs,\n    enabled,\n    reducedMotion,\n    renderData,\n    xAccessor,\n    xScale,\n    yScale,\n    dataKey,\n    targetPoints,\n  ]);\n\n  const activePoints = animatedPoints ?? targetPoints;\n  const pathD = useMemo(\n    () => seriesPathFromPoints(activePoints, curve),\n    [activePoints, curve]\n  );\n\n  return {\n    pathD,\n    isPathAnimating: animatedPoints != null,\n  };\n}\n",
      "type": "registry:component",
      "target": "components/charts/use-animated-series-path.ts"
    },
    {
      "path": "src/charts/highlight-segment-bounds.ts",
      "content": "import type { TooltipData } from \"./chart-context\";\nimport type { ChartSelection } from \"./use-chart-interaction\";\n\n// Pure geometry for the hover-highlight band, split out from the hook so it can\n// be unit-tested without React/motion (see __tests__).\n//\n// The band is the pixel x-range one data point either side of the hovered point:\n//   [ xScale(t(idx-1)), xScale(t(idx+1)) ]\n// `<HighlightSegment>` then re-strokes the base path clipped to that band, so the\n// highlight always traces the line itself. Selecting the band by data index\n// assumes x is monotone along the path, which holds for a time series. On a curve\n// that overshoots in x (curveNatural, curveBasis) a band edge can land a few\n// pixels short, slightly narrowing the bright slice but never detaching it.\n\nexport interface SegmentBounds {\n  /** Left edge of the highlight band, in pixels. */\n  x: number;\n  /** Width of the highlight band, in pixels. */\n  width: number;\n  isActive: boolean;\n}\n\nexport const INACTIVE_SEGMENT: SegmentBounds = {\n  x: 0,\n  width: 0,\n  isActive: false,\n};\n\n/**\n * The highlight band `{x, width}` in pixel space, from the data + `xScale` plus\n * the current hover/selection. Hover spans one data point either side of the dot\n * (clamped to the ends); an active drag-selection uses the dragged pixel range\n * directly and takes priority over hover.\n */\nexport function computeSegmentBounds(\n  data: Record<string, unknown>[],\n  xScale: (value: Date) => number | undefined,\n  xAccessor: (d: Record<string, unknown>) => Date,\n  tooltipData: Pick<TooltipData, \"index\"> | null | undefined,\n  selection:\n    | Pick<ChartSelection, \"active\" | \"startX\" | \"endX\">\n    | null\n    | undefined\n): SegmentBounds {\n  if (data.length === 0) {\n    return INACTIVE_SEGMENT;\n  }\n\n  if (selection?.active) {\n    const x = Math.min(selection.startX, selection.endX);\n    const width = Math.abs(selection.endX - selection.startX);\n    return { x, width, isActive: true };\n  }\n\n  if (!tooltipData) {\n    return INACTIVE_SEGMENT;\n  }\n\n  const idx = tooltipData.index;\n  const startIdx = Math.max(0, idx - 1);\n  const endIdx = Math.min(data.length - 1, idx + 1);\n  const startPoint = data[startIdx];\n  const endPoint = data[endIdx];\n  if (!(startPoint && endPoint)) {\n    return INACTIVE_SEGMENT;\n  }\n\n  const startX = xScale(xAccessor(startPoint)) ?? 0;\n  const endX = xScale(xAccessor(endPoint)) ?? 0;\n  return { x: startX, width: Math.max(0, endX - startX), isActive: true };\n}\n",
      "type": "registry:component",
      "target": "components/charts/highlight-segment-bounds.ts"
    },
    {
      "path": "src/charts/highlight-segment.tsx",
      "content": "\"use client\";\n\nimport { type MotionValue, motion } from \"motion/react\";\nimport { type RefObject, useId } from \"react\";\n\n// Hover-highlight overlay: re-strokes the base path `d`, clipped to a vertical\n// band whose x/width spring to track the hovered point, so only the segment\n// around the dot shows brighter. The band comes from `useHighlightSegment`;\n// because the bright stroke reuses the base `d`, it follows whatever curve is\n// drawn (see `highlight-segment-bounds.ts` for the band-extent caveat).\n\nexport interface HighlightSegmentProps {\n  /** Ref to the rendered base stroke `<path>` — its `d` is re-used verbatim. */\n  pathRef: RefObject<SVGPathElement | null>;\n  /** Whether to render (caller gates on showHighlight + active + loaded). */\n  visible: boolean;\n  stroke: string;\n  strokeWidth: number;\n  /** Plot height — the clip band spans it fully. */\n  height: number;\n  /** Spring-eased left edge of the clip band (px). */\n  x: MotionValue<number>;\n  /** Spring-eased width of the clip band (px). */\n  width: MotionValue<number>;\n}\n\nexport function HighlightSegment({\n  pathRef,\n  visible,\n  stroke,\n  strokeWidth,\n  height,\n  x,\n  width,\n}: HighlightSegmentProps) {\n  const clipId = useId();\n  if (!(visible && pathRef.current)) {\n    return null;\n  }\n  return (\n    <>\n      <defs>\n        <clipPath id={clipId}>\n          <motion.rect height={height} width={width} x={x} y={0} />\n        </clipPath>\n      </defs>\n      <motion.path\n        animate={{ opacity: 1 }}\n        clipPath={`url(#${clipId})`}\n        d={pathRef.current.getAttribute(\"d\") || \"\"}\n        exit={{ opacity: 0 }}\n        fill=\"none\"\n        initial={{ opacity: 0 }}\n        stroke={stroke}\n        strokeLinecap=\"round\"\n        strokeWidth={strokeWidth}\n        transition={{ duration: 0.4, ease: \"easeInOut\" }}\n      />\n    </>\n  );\n}\n\nHighlightSegment.displayName = \"HighlightSegment\";\n\nexport default HighlightSegment;\n",
      "type": "registry:component",
      "target": "components/charts/highlight-segment.tsx"
    },
    {
      "path": "src/charts/dash-tail-stroke.tsx",
      "content": "\"use client\";\n\nimport { useId } from \"react\";\n\nexport interface DashTailStrokeProps {\n  /** SVG path `d` for the full series (single curved path). */\n  pathD: string | null;\n  /** Total length of `pathD` in user units. */\n  pathLength: number;\n  /** Path length at which the dashed tail begins. */\n  dashStartLength: number;\n  /** X coordinate (chart inner space) where the tail clip begins. */\n  dashStartX: number;\n  innerWidth: number;\n  innerHeight: number;\n  /** Stroke paint — solid color or gradient url. */\n  stroke: string;\n  strokeWidth: number;\n  dashArray: string;\n}\n\nexport function DashTailStroke({\n  pathD,\n  pathLength,\n  dashStartLength,\n  dashStartX,\n  innerWidth,\n  innerHeight,\n  stroke,\n  strokeWidth,\n  dashArray,\n}: DashTailStrokeProps) {\n  const clipPathId = useId().replace(/:/g, \"\");\n\n  if (!pathD || pathLength <= 0 || dashStartLength >= pathLength) {\n    return null;\n  }\n\n  const pad = strokeWidth * 2;\n  const tailWidth = Math.max(0, innerWidth - dashStartX + pad);\n\n  return (\n    <>\n      <defs>\n        <clipPath id={clipPathId}>\n          <rect\n            height={innerHeight + pad}\n            width={tailWidth}\n            x={dashStartX - strokeWidth}\n            y={-strokeWidth}\n          />\n        </clipPath>\n      </defs>\n      {/* Solid head — same curved path, gradient/fade preserved */}\n      <path\n        d={pathD}\n        fill=\"none\"\n        stroke={stroke}\n        strokeDasharray={`${dashStartLength} ${Math.max(1, pathLength - dashStartLength)}`}\n        strokeLinecap=\"round\"\n        strokeWidth={strokeWidth}\n      />\n      {/* Dashed tail — clipped to x ≥ dashStartX so dashes follow the curve */}\n      <path\n        clipPath={`url(#${clipPathId})`}\n        d={pathD}\n        fill=\"none\"\n        stroke={stroke}\n        strokeDasharray={dashArray}\n        strokeLinecap=\"round\"\n        strokeWidth={strokeWidth}\n      />\n    </>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/charts/dash-tail-stroke.tsx"
    },
    {
      "path": "src/charts/series-dash-tail-overlay.tsx",
      "content": "\"use client\";\n\nimport { memo, useMemo } from \"react\";\nimport { DashTailStroke } from \"./dash-tail-stroke\";\nimport { resolveDashStartX, resolveDashTailBounds } from \"./path-stroke-utils\";\n\ninterface SeriesDashTailOverlayProps {\n  dashFromIndex?: number;\n  dashArray: string;\n  data: Record<string, unknown>[];\n  pathD: string | null;\n  pathLength: number;\n  innerWidth: number;\n  innerHeight: number;\n  stroke: string;\n  strokeWidth: number;\n  xScale: (value: Date | number) => number | undefined;\n  xAccessor: (datum: Record<string, unknown>) => Date | number;\n}\n\nfunction SeriesDashTailOverlayImpl({\n  dashFromIndex,\n  dashArray,\n  data,\n  pathD,\n  pathLength,\n  innerWidth,\n  innerHeight,\n  stroke,\n  strokeWidth,\n  xScale,\n  xAccessor,\n}: SeriesDashTailOverlayProps) {\n  const hasDashTail = resolveDashTailBounds(dashFromIndex, data.length);\n\n  const dashStartX = useMemo(() => {\n    if (!hasDashTail || dashFromIndex == null) {\n      return 0;\n    }\n    return resolveDashStartX(data, dashFromIndex, xScale, xAccessor);\n  }, [hasDashTail, dashFromIndex, data, xScale, xAccessor]);\n\n  // Linear (index-based) approximation of the path length at `dashFromIndex`.\n  // The accurate version (`findPathLengthAtX` binary search via\n  // `getPointAtLength`) is exact but cost ~40 ms per series on a 365-point\n  // bezier — for charts with ~10 series that synchronously blocks the main\n  // thread for ~400 ms on the post-measurement re-render, swallowing the first\n  // second of the entrance animation.\n  //\n  // For evenly-spaced time-series data — the standard case — this is exact at\n  // flat regions of the curve and only differs by a pixel or two where the\n  // curve has steep y-variation, which is imperceptible at the dash boundary.\n  const dashStartLength = useMemo(() => {\n    if (!hasDashTail || dashFromIndex == null || pathLength <= 0) {\n      return 0;\n    }\n    return (dashFromIndex / Math.max(1, data.length - 1)) * pathLength;\n  }, [hasDashTail, dashFromIndex, data.length, pathLength]);\n\n  if (!hasDashTail || dashFromIndex == null || pathLength <= 0) {\n    return null;\n  }\n\n  return (\n    <DashTailStroke\n      dashArray={dashArray}\n      dashStartLength={dashStartLength}\n      dashStartX={dashStartX}\n      innerHeight={innerHeight}\n      innerWidth={innerWidth}\n      pathD={pathD}\n      pathLength={pathLength}\n      stroke={stroke}\n      strokeWidth={strokeWidth}\n    />\n  );\n}\n\n// All props originate from the chart's stable context slice (data, xScale,\n// xAccessor, …) or are mount-stable strings (gradient `url(#…)` ids). Shallow\n// compare lets us skip the path-length binary search on every cursor move.\nexport const SeriesDashTailOverlay = memo(SeriesDashTailOverlayImpl);\n",
      "type": "registry:component",
      "target": "components/charts/series-dash-tail-overlay.tsx"
    },
    {
      "path": "src/charts/series-highlight-layer.tsx",
      "content": "\"use client\";\n\nimport type { RefObject } from \"react\";\nimport { useChartStable } from \"./chart-context\";\nimport { HighlightSegment } from \"./highlight-segment\";\nimport { useHighlightSegment } from \"./use-highlight-segment\";\n\ninterface SeriesHighlightLayerProps {\n  /** Caller already gated `showHighlight && showLine`; this just routes through. */\n  enabled: boolean;\n  height: number;\n  pathRef: RefObject<SVGPathElement | null>;\n  stroke: string;\n  strokeWidth: number;\n}\n\n/**\n * Self-contained hover-highlight band over a series stroke.\n *\n * Owns the `useHighlightSegment` subscription (which reads both stable + hover\n * context) so the parent <Area> / <Line> can stay on the stable slice. This\n * component still re-renders on hover — that's the price of driving the\n * highlight band — but it's a tiny leaf so the cost is bounded to itself.\n */\nexport function SeriesHighlightLayer({\n  enabled,\n  height,\n  pathRef,\n  stroke,\n  strokeWidth,\n}: SeriesHighlightLayerProps) {\n  const { isLoaded } = useChartStable();\n  const { xSpring, widthSpring, isActive } = useHighlightSegment({ enabled });\n  return (\n    <HighlightSegment\n      height={height}\n      pathRef={pathRef}\n      stroke={stroke}\n      strokeWidth={strokeWidth}\n      visible={enabled && isActive && isLoaded}\n      width={widthSpring}\n      x={xSpring}\n    />\n  );\n}\n\nSeriesHighlightLayer.displayName = \"SeriesHighlightLayer\";\n\nexport default SeriesHighlightLayer;\n",
      "type": "registry:component",
      "target": "components/charts/series-highlight-layer.tsx"
    },
    {
      "path": "src/charts/chart-legend-hover.tsx",
      "content": "\"use client\";\n\nimport { createContext, type ReactNode, useContext, useMemo } from \"react\";\n\ninterface ChartLegendHoverContextValue {\n  hoveredIndex: number | null;\n  setHoveredIndex: (index: number | null) => void;\n}\n\nconst ChartLegendHoverContext =\n  createContext<ChartLegendHoverContextValue | null>(null);\n\nexport function ChartLegendHoverProvider({\n  hoveredIndex,\n  onHoverChange,\n  children,\n}: {\n  hoveredIndex: number | null;\n  onHoverChange: (index: number | null) => void;\n  children: ReactNode;\n}) {\n  const value = useMemo(\n    () => ({ hoveredIndex, setHoveredIndex: onHoverChange }),\n    [hoveredIndex, onHoverChange]\n  );\n\n  return (\n    <ChartLegendHoverContext.Provider value={value}>\n      {children}\n    </ChartLegendHoverContext.Provider>\n  );\n}\n\nexport function useChartLegendHover(): ChartLegendHoverContextValue {\n  const context = useContext(ChartLegendHoverContext);\n  return (\n    context ?? {\n      hoveredIndex: null,\n      setHoveredIndex: () => {\n        /* noop outside ChartLegendHoverProvider */\n      },\n    }\n  );\n}\n",
      "type": "registry:component",
      "target": "components/charts/chart-legend-hover.tsx"
    },
    {
      "path": "src/charts/series-hover-dim.tsx",
      "content": "\"use client\";\n\nimport { motion } from \"motion/react\";\nimport type { ReactNode } from \"react\";\nimport { useChartHover } from \"./chart-context\";\nimport { useChartLegendHover } from \"./chart-legend-hover\";\n\ninterface SeriesHoverDimProps {\n  /** Skip the dim entirely. */\n  enabled?: boolean;\n  /** Opacity to fade to while the chart is being hovered. */\n  dimOpacity?: number;\n  /** Tween duration in seconds. */\n  durationSec?: number;\n  /** Series index for multi-series legend hover dimming. */\n  seriesIndex?: number;\n  /** Stable chart visuals — area fill, stroke line, dashed tail, etc. */\n  children: ReactNode;\n}\n\n/**\n * Wraps stable series visuals with a hover-driven opacity animation.\n *\n * The wrapper subscribes to chart hover state internally so the parent (Area /\n * Line) can stay on the stable context slice. Children come in as a React prop:\n * because the parent is not re-rendering on hover, the children element\n * reference stays identical and React skips re-rendering them when this\n * wrapper re-renders. That keeps expensive subtrees (`SeriesDashTailOverlay`\n * and its `getPointAtLength` binary search) quiescent on cursor motion.\n */\nexport function SeriesHoverDim({\n  enabled = true,\n  dimOpacity = 0.5,\n  durationSec = 0.4,\n  seriesIndex,\n  children,\n}: SeriesHoverDimProps) {\n  const { tooltipData, selection } = useChartHover();\n  const { hoveredIndex: legendHoveredIndex } = useChartLegendHover();\n  const isChartHovering = tooltipData !== null || selection?.active === true;\n  const isLegendDimmed =\n    legendHoveredIndex !== null &&\n    seriesIndex !== undefined &&\n    legendHoveredIndex !== seriesIndex;\n  const opacity =\n    enabled && (isChartHovering || isLegendDimmed) ? dimOpacity : 1;\n  return (\n    <motion.g\n      animate={{ opacity }}\n      initial={{ opacity: 1 }}\n      transition={{ duration: durationSec, ease: \"easeInOut\" }}\n    >\n      {children}\n    </motion.g>\n  );\n}\n\nSeriesHoverDim.displayName = \"SeriesHoverDim\";\n\nexport default SeriesHoverDim;\n",
      "type": "registry:component",
      "target": "components/charts/series-hover-dim.tsx"
    },
    {
      "path": "src/charts/series-point-marker.tsx",
      "content": "\"use client\";\n\nimport type { Variants } from \"motion/react\";\nimport { motion } from \"motion/react\";\nimport { memo } from \"react\";\nimport { DEFAULT_CHART_ENTER_TRANSITION } from \"./animation\";\n\nexport interface SeriesPointMarkerStyle {\n  /** Fill color for the inner circle */\n  fill?: string;\n  /** Outer ring stroke color. Default: same as `fill` */\n  stroke?: string;\n  /** Outer ring stroke width in px. Default: 2. Set to 0 to disable. */\n  strokeWidth?: number;\n  /** Gap between the inner fill and outer ring in px. Default: 2 */\n  ringGap?: number;\n  /** Optional outer outline beyond the ring. Default: 0 */\n  outlineWidth?: number;\n  /** Outer outline color. Default: same as `stroke` */\n  outlineColor?: string;\n  /** Point radius in px. Default: 5 */\n  radius?: number;\n  /** Dim non-active points while hovering. Default: true */\n  fadeOnHover?: boolean;\n  /** Opacity for non-hovered points when `fadeOnHover` is true. Default: 0.5 */\n  inactiveOpacity?: number;\n  /**\n   * Blur in px for non-hovered points when `fadeOnHover` is true.\n   * Applied once on the dimmed layer (not per dot) for performance. Default: 2\n   */\n  inactiveBlur?: number;\n  /** Initial blur in px during enter animation. Default: 2 */\n  enterBlur?: number;\n  /** Enlarge the active point while hovering. Default: true */\n  showActiveHighlight?: boolean;\n}\n\ninterface MarkerCirclesProps {\n  fill?: string;\n  stroke?: string;\n  strokeWidth: number;\n  ringGap: number;\n  outlineWidth: number;\n  outlineColor?: string;\n  radius: number;\n}\n\nfunction MarkerCircles({\n  fill,\n  stroke,\n  strokeWidth,\n  ringGap,\n  outlineWidth,\n  outlineColor,\n  radius,\n}: MarkerCirclesProps) {\n  const resolvedStroke = stroke ?? fill ?? \"currentColor\";\n  const resolvedOutlineColor = outlineColor ?? resolvedStroke;\n  const ringOuter = strokeWidth > 0 ? radius + ringGap + strokeWidth : radius;\n  const outlineRadius = outlineWidth > 0 ? ringOuter + outlineWidth / 2 : 0;\n\n  return (\n    <>\n      {outlineWidth > 0 ? (\n        <circle\n          cx={0}\n          cy={0}\n          fill=\"none\"\n          r={outlineRadius}\n          stroke={resolvedOutlineColor}\n          strokeWidth={outlineWidth}\n        />\n      ) : null}\n      <circle cx={0} cy={0} fill={fill} r={radius} />\n      {strokeWidth > 0 ? (\n        <circle\n          cx={0}\n          cy={0}\n          fill=\"none\"\n          r={radius + ringGap + strokeWidth / 2}\n          stroke={resolvedStroke}\n          strokeWidth={strokeWidth}\n        />\n      ) : null}\n    </>\n  );\n}\n\nexport interface StaticSeriesPointMarkerProps extends SeriesPointMarkerStyle {\n  cx: number;\n  cy: number;\n  scale?: number;\n}\n\nexport const StaticSeriesPointMarker = memo(function StaticSeriesPointMarker({\n  cx,\n  cy,\n  scale = 1,\n  fill,\n  stroke,\n  strokeWidth = 2,\n  ringGap = 2,\n  outlineWidth = 0,\n  outlineColor,\n  radius = 5,\n}: StaticSeriesPointMarkerProps) {\n  return (\n    <g transform={`translate(${cx}, ${cy}) scale(${scale})`}>\n      <MarkerCircles\n        fill={fill}\n        outlineColor={outlineColor}\n        outlineWidth={outlineWidth}\n        radius={radius}\n        ringGap={ringGap}\n        stroke={stroke}\n        strokeWidth={strokeWidth}\n      />\n    </g>\n  );\n});\n\nexport interface SeriesPointMarkerProps extends SeriesPointMarkerStyle {\n  dataKey: string;\n  index: number;\n  cx: number;\n  cy: number;\n  revealDelay: number;\n  revealEpoch: number;\n  enterDuration: number;\n}\n\n/** Motion enter marker — used only while the chart reveal is running. */\nexport function SeriesPointMarker({\n  dataKey,\n  index,\n  cx,\n  cy,\n  enterBlur = 2,\n  revealDelay,\n  revealEpoch,\n  enterDuration,\n  fill,\n  stroke,\n  strokeWidth = 2,\n  ringGap = 2,\n  outlineWidth = 0,\n  outlineColor,\n  radius = 5,\n}: SeriesPointMarkerProps) {\n  const variants: Variants = {\n    hidden: {\n      opacity: 0,\n      filter: `blur(${enterBlur}px)`,\n      scale: 1,\n    },\n    visible: {\n      opacity: 1,\n      filter: \"blur(0px)\",\n      scale: 1,\n      transition: {\n        delay: revealDelay,\n        duration: enterDuration,\n        ease: DEFAULT_CHART_ENTER_TRANSITION.ease,\n      },\n    },\n  };\n\n  return (\n    <g transform={`translate(${cx}, ${cy})`}>\n      <motion.g\n        animate=\"visible\"\n        initial=\"hidden\"\n        key={`${dataKey}-${index}-${revealEpoch}`}\n        variants={variants}\n      >\n        <MarkerCircles\n          fill={fill}\n          outlineColor={outlineColor}\n          outlineWidth={outlineWidth}\n          radius={radius}\n          ringGap={ringGap}\n          stroke={stroke}\n          strokeWidth={strokeWidth}\n        />\n      </motion.g>\n    </g>\n  );\n}\n\nexport function getSeriesMarkerVisualExtent(\n  style: Pick<\n    SeriesPointMarkerStyle,\n    | \"radius\"\n    | \"strokeWidth\"\n    | \"ringGap\"\n    | \"outlineWidth\"\n    | \"showActiveHighlight\"\n  >\n): number {\n  const radius = style.radius ?? 5;\n  const strokeWidth = style.strokeWidth ?? 2;\n  const ringGap = style.ringGap ?? 2;\n  const outlineWidth = style.outlineWidth ?? 0;\n  const showActiveHighlight = style.showActiveHighlight ?? true;\n  const ring = strokeWidth > 0 ? ringGap + strokeWidth : 0;\n  const outline = outlineWidth > 0 ? outlineWidth : 0;\n  const highlightPad = showActiveHighlight ? radius * 0.35 : 0;\n  return radius + ring + outline + highlightPad + 2;\n}\n",
      "type": "registry:component",
      "target": "components/charts/series-point-marker.tsx"
    },
    {
      "path": "src/charts/series-markers.tsx",
      "content": "\"use client\";\n\nimport { type ReactNode, useCallback, useMemo } from \"react\";\nimport { clipRevealTransition } from \"./animation\";\nimport {\n  defaultScatterColors,\n  useChartHover,\n  useChartStable,\n  useYScale,\n} from \"./chart-context\";\nimport { useChartLegendHover } from \"./chart-legend-hover\";\nimport {\n  getSeriesMarkerVisualExtent,\n  SeriesPointMarker,\n  type SeriesPointMarkerStyle,\n  StaticSeriesPointMarker,\n} from \"./series-point-marker\";\n\nexport interface SeriesMarkersProps extends SeriesPointMarkerStyle {\n  dataKey: string;\n  /** Marker fill color. Defaults to series stroke or chart palette color. */\n  fill?: string;\n  /** Whether to animate markers with clip reveal. Default: true */\n  animate?: boolean;\n}\n\ninterface PointAt {\n  index: number;\n  cx: number;\n  cy: number;\n  revealDelay: number;\n}\n\ninterface MarkerStyle {\n  fill: string;\n  stroke: string;\n  strokeWidth: number;\n  ringGap: number;\n  outlineWidth: number;\n  outlineColor?: string;\n  radius: number;\n}\n\nexport function SeriesMarkers({\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}: SeriesMarkersProps) {\n  // Stable slice only. Hover-driven dim + active-highlight live in the inner\n  // <SeriesMarkersDimWrapper> / <SeriesMarkersActiveHighlight> components, so\n  // mouse motion does not re-render the full point grid.\n  const {\n    data,\n    xScale,\n    innerWidth,\n    enterTransition,\n    animationDuration,\n    revealEpoch,\n    isLoaded,\n    xAccessor,\n    lines,\n  } = useChartStable();\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 seriesConfig = lines[seriesIndex];\n  const yScale = useYScale(seriesConfig?.yAxisId);\n  const seriesColor =\n    defaultScatterColors[seriesIndex % defaultScatterColors.length] ??\n    defaultScatterColors[0];\n\n  const resolvedFill = fill ?? seriesConfig?.stroke ?? seriesColor;\n  const resolvedStroke = stroke ?? resolvedFill;\n\n  const visualExtent = useMemo(\n    () =>\n      getSeriesMarkerVisualExtent({\n        radius,\n        strokeWidth,\n        ringGap,\n        outlineWidth,\n        showActiveHighlight,\n      }),\n    [radius, strokeWidth, ringGap, outlineWidth, showActiveHighlight]\n  );\n\n  const revealDurationSec =\n    clipRevealTransition(enterTransition).duration ?? animationDuration / 1000;\n  const enterDuration = 0.5;\n  const isRevealing = animate && !isLoaded;\n\n  const getY = useCallback(\n    (d: Record<string, unknown>) => {\n      const value = d[dataKey];\n      return typeof value === \"number\" ? (yScale(value) ?? 0) : null;\n    },\n    [dataKey, yScale]\n  );\n\n  const points = useMemo<PointAt[]>(\n    () =>\n      data.flatMap((d, index) => {\n        const cy = getY(d);\n        if (cy === null) {\n          return [];\n        }\n        const cx = xScale(xAccessor(d)) ?? 0;\n        const leadingEdge = Math.max(0, cx - visualExtent);\n        const revealDelay =\n          innerWidth > 0 && isRevealing\n            ? (leadingEdge / innerWidth) * revealDurationSec\n            : 0;\n\n        return [{ index, cx, cy, revealDelay }];\n      }),\n    [\n      data,\n      getY,\n      xScale,\n      xAccessor,\n      innerWidth,\n      isRevealing,\n      revealDurationSec,\n      visualExtent,\n    ]\n  );\n\n  // Memo so the inner <SeriesMarkersActiveHighlight> sees a stable prop and\n  // can be cheaply re-rendered on hover without re-creating the spread.\n  const markerStyle = useMemo<MarkerStyle>(\n    () => ({\n      fill: resolvedFill,\n      stroke: resolvedStroke,\n      strokeWidth,\n      ringGap,\n      outlineWidth,\n      outlineColor,\n      radius,\n    }),\n    [\n      resolvedFill,\n      resolvedStroke,\n      strokeWidth,\n      ringGap,\n      outlineWidth,\n      outlineColor,\n      radius,\n    ]\n  );\n\n  if (isRevealing) {\n    return (\n      <g>\n        {points.map((point) => (\n          <SeriesPointMarker\n            cx={point.cx}\n            cy={point.cy}\n            dataKey={dataKey}\n            enterBlur={enterBlur}\n            enterDuration={enterDuration}\n            index={point.index}\n            key={`${dataKey}-${point.index}`}\n            revealDelay={point.revealDelay}\n            revealEpoch={revealEpoch ?? 0}\n            {...markerStyle}\n          />\n        ))}\n      </g>\n    );\n  }\n\n  // Stable base layer — its children come from the parent and stay\n  // referentially identical when the dim wrapper re-renders for hover.\n  const baseMarkers = points.map((point) => (\n    <StaticSeriesPointMarker\n      cx={point.cx}\n      cy={point.cy}\n      key={`${dataKey}-${point.index}`}\n      {...markerStyle}\n    />\n  ));\n  const activeScale = showActiveHighlight ? 1.35 : 1;\n\n  return (\n    <g>\n      <SeriesMarkersDimWrapper\n        enabled={fadeOnHover}\n        inactiveBlur={inactiveBlur}\n        inactiveOpacity={inactiveOpacity}\n        seriesIndex={seriesIndex}\n      >\n        {baseMarkers}\n      </SeriesMarkersDimWrapper>\n      <SeriesMarkersActiveHighlight\n        activeScale={activeScale}\n        enabled={fadeOnHover}\n        markerStyle={markerStyle}\n        points={points}\n      />\n    </g>\n  );\n}\n\nSeriesMarkers.displayName = \"SeriesMarkers\";\n\ninterface SeriesMarkersDimWrapperProps {\n  enabled: boolean;\n  inactiveOpacity: number;\n  inactiveBlur: number;\n  seriesIndex: number;\n  children: ReactNode;\n}\n\n/**\n * Wraps the stable point grid with hover-driven opacity + blur. Subscribes to\n * hover internally so the grid (passed as `children`) keeps a stable reference\n * and React skips reconciling it when this wrapper re-renders.\n */\nfunction SeriesMarkersDimWrapper({\n  enabled,\n  inactiveOpacity,\n  inactiveBlur,\n  seriesIndex,\n  children,\n}: SeriesMarkersDimWrapperProps) {\n  const { tooltipData } = useChartHover();\n  const { hoveredIndex: legendHoveredIndex } = useChartLegendHover();\n  const isLegendDimmed =\n    legendHoveredIndex !== null && legendHoveredIndex !== seriesIndex;\n  const dimBase = enabled && (tooltipData !== null || isLegendDimmed);\n  return (\n    <g\n      opacity={dimBase ? inactiveOpacity : 1}\n      style={{\n        transition: \"opacity 0.15s ease-in-out, filter 0.15s ease-in-out\",\n        filter:\n          dimBase && inactiveBlur > 0 ? `blur(${inactiveBlur}px)` : \"none\",\n      }}\n    >\n      {children}\n    </g>\n  );\n}\n\ninterface SeriesMarkersActiveHighlightProps {\n  enabled: boolean;\n  points: PointAt[];\n  markerStyle: MarkerStyle;\n  activeScale: number;\n}\n\n/**\n * Renders the scaled \"active\" marker on top of the base grid. Subscribes to\n * hover internally; the parent doesn't re-render on cursor motion.\n */\nfunction SeriesMarkersActiveHighlight({\n  enabled,\n  points,\n  markerStyle,\n  activeScale,\n}: SeriesMarkersActiveHighlightProps) {\n  const { tooltipData } = useChartHover();\n  if (!enabled || tooltipData === null) {\n    return null;\n  }\n  const activePoint = points.find((point) => point.index === tooltipData.index);\n  if (!activePoint) {\n    return null;\n  }\n  return (\n    <StaticSeriesPointMarker\n      cx={activePoint.cx}\n      cy={activePoint.cy}\n      scale={activeScale}\n      {...markerStyle}\n    />\n  );\n}\n\nexport default SeriesMarkers;\n",
      "type": "registry:component",
      "target": "components/charts/series-markers.tsx"
    },
    {
      "path": "src/charts/use-highlight-segment.ts",
      "content": "\"use client\";\n\nimport { useSpring } from \"motion/react\";\nimport { useMemo, useRef } from \"react\";\nimport { useChartConfig } from \"./chart-config-context\";\nimport { useChartHover, useChartStable } from \"./chart-context\";\nimport {\n  computeSegmentBounds,\n  INACTIVE_SEGMENT,\n} from \"./highlight-segment-bounds\";\n\n// Hover-highlight band for `line.tsx` and `area.tsx`. Computes the segment\n// bounds and springs its x/width; `<HighlightSegment>` renders the clipped\n// re-stroke. Spring tuning comes from `ChartConfigProvider.highlightSpring`.\n// Stable + hover slices are read separately so callers can see the exact\n// subscription surface (anything calling this hook will re-render on hover).\n\nexport interface HighlightSegmentResult {\n  xSpring: ReturnType<typeof useSpring>;\n  widthSpring: ReturnType<typeof useSpring>;\n  isActive: boolean;\n}\n\n/**\n * @param enabled set false when there is no stroke to highlight (e.g. an area\n *   with `showLine={false}`); defaults true.\n */\nexport function useHighlightSegment({\n  enabled = true,\n}: {\n  enabled?: boolean;\n} = {}): HighlightSegmentResult {\n  const { data, xScale, xAccessor } = useChartStable();\n  const { tooltipData, selection } = useChartHover();\n  const { highlightSpring } = useChartConfig();\n\n  const bounds = useMemo(\n    () =>\n      enabled\n        ? computeSegmentBounds(data, xScale, xAccessor, tooltipData, selection)\n        : INACTIVE_SEGMENT,\n    [enabled, data, xScale, xAccessor, tooltipData, selection]\n  );\n\n  const xSpring = useSpring(0, highlightSpring);\n  const widthSpring = useSpring(0, highlightSpring);\n\n  // Jump on inactive→active so the band appears at the hovered point instead\n  // of sliding in from x=0; ease on subsequent moves.\n  const wasActive = useRef(false);\n  if (bounds.isActive && !wasActive.current) {\n    xSpring.jump(bounds.x);\n    widthSpring.jump(bounds.width);\n  } else {\n    xSpring.set(bounds.x);\n    widthSpring.set(bounds.width);\n  }\n  wasActive.current = bounds.isActive;\n\n  return { xSpring, widthSpring, isActive: bounds.isActive };\n}\n",
      "type": "registry:component",
      "target": "components/charts/use-highlight-segment.ts"
    },
    {
      "path": "src/charts/area-gradient-defs.tsx",
      "content": "import {\n  type FadeEdges,\n  fadeGradientStops,\n  resolveFadeSides,\n  viewportFadeGradientAttrs,\n} from \"./fade-edges\";\n\ninterface AreaGradientDefsProps {\n  gradientId: string;\n  strokeGradientId: string;\n  edgeMaskId: string;\n  edgeGradientId: string;\n  fill: string;\n  fillOpacity: number;\n  gradientToOpacity: number;\n  /** 0–1: where the bottom stop sits (1 = full-height gradient). */\n  gradientSpan?: number;\n  resolvedStroke: string;\n  isPatternFill: boolean;\n  fadeEdges: FadeEdges;\n  innerWidth: number;\n  innerHeight: number;\n}\n\nexport function AreaGradientDefs({\n  gradientId,\n  strokeGradientId,\n  edgeMaskId,\n  edgeGradientId,\n  fill,\n  fillOpacity,\n  gradientToOpacity,\n  gradientSpan = 1,\n  resolvedStroke,\n  isPatternFill,\n  fadeEdges,\n  innerWidth,\n  innerHeight,\n}: AreaGradientDefsProps) {\n  const sides = resolveFadeSides(fadeEdges);\n  // Stroke gradient mirrors the area's edge fade so the line doesn't pop in\n  // past the faded fill. Skip emitting it when neither edge fades — the line\n  // can then paint a solid stroke instead of an unnecessary url(#...) ref.\n  const strokeStops = sides.any ? fadeGradientStops(sides) : null;\n  const showEdgeMask = sides.any && !isPatternFill;\n  const edgeStops = showEdgeMask ? fadeGradientStops(sides) : null;\n  const span = Math.min(1, Math.max(0.01, gradientSpan));\n  const midOffset = `${span * 100}%`;\n\n  return (\n    <defs>\n      {isPatternFill ? null : (\n        <linearGradient id={gradientId} x1=\"0%\" x2=\"0%\" y1=\"0%\" y2=\"100%\">\n          <stop\n            offset=\"0%\"\n            style={{ stopColor: fill, stopOpacity: fillOpacity }}\n          />\n          <stop\n            offset={midOffset}\n            style={{ stopColor: fill, stopOpacity: gradientToOpacity }}\n          />\n          {span < 1 ? (\n            <stop\n              offset=\"100%\"\n              style={{ stopColor: fill, stopOpacity: gradientToOpacity }}\n            />\n          ) : null}\n        </linearGradient>\n      )}\n\n      {strokeStops ? (\n        <linearGradient\n          id={strokeGradientId}\n          {...viewportFadeGradientAttrs(innerWidth)}\n        >\n          {strokeStops.map((stop) => (\n            <stop\n              key={stop.offset}\n              offset={stop.offset}\n              style={{ stopColor: resolvedStroke, stopOpacity: stop.opacity }}\n            />\n          ))}\n        </linearGradient>\n      ) : null}\n\n      {edgeStops ? (\n        <>\n          <linearGradient\n            id={edgeGradientId}\n            {...viewportFadeGradientAttrs(innerWidth)}\n          >\n            {edgeStops.map((stop) => (\n              <stop\n                key={stop.offset}\n                offset={stop.offset}\n                style={{ stopColor: \"white\", stopOpacity: stop.opacity }}\n              />\n            ))}\n          </linearGradient>\n          <mask id={edgeMaskId}>\n            <rect\n              fill={`url(#${edgeGradientId})`}\n              height={innerHeight}\n              width={innerWidth}\n              x=\"0\"\n              y=\"0\"\n            />\n          </mask>\n        </>\n      ) : null}\n    </defs>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/charts/area-gradient-defs.tsx"
    },
    {
      "path": "src/charts/fade-edges.ts",
      "content": "/**\n * Which side(s) of a series should fade to transparent at the chart edges.\n * - `true`  → fade both edges (default for `<Line>`)\n * - `false` → no fade (default for `<Area>`)\n * - `\"left\"` / `\"right\"` → fade only that side\n */\nexport type FadeEdges = boolean | \"left\" | \"right\";\n\nexport interface FadeSides {\n  /** Whether the left edge should fade out. */\n  left: boolean;\n  /** Whether the right edge should fade out. */\n  right: boolean;\n  /** True if either side fades — use to gate gradient/mask defs. */\n  any: boolean;\n}\n\nexport function resolveFadeSides(fade: FadeEdges): FadeSides {\n  if (fade === false) {\n    return { left: false, right: false, any: false };\n  }\n  if (fade === \"left\") {\n    return { left: true, right: false, any: true };\n  }\n  if (fade === \"right\") {\n    return { left: false, right: true, any: true };\n  }\n  return { left: true, right: true, any: true };\n}\n\nexport interface FadeGradientStop {\n  offset: string;\n  opacity: number;\n}\n\n/**\n * Stops for a horizontal fade gradient with opacity 0 at the faded side(s)\n * and opacity 1 in the middle. Matches the historic 0/15/85/100 pattern.\n */\nexport function fadeGradientStops(sides: FadeSides): FadeGradientStop[] {\n  return [\n    { offset: \"0%\", opacity: sides.left ? 0 : 1 },\n    { offset: \"15%\", opacity: 1 },\n    { offset: \"85%\", opacity: 1 },\n    { offset: \"100%\", opacity: sides.right ? 0 : 1 },\n  ];\n}\n\n/** Horizontal fade gradient pinned to the chart viewport (not the series path bounds). */\nexport function viewportFadeGradientAttrs(innerWidth: number) {\n  return {\n    gradientUnits: \"userSpaceOnUse\" as const,\n    x1: 0,\n    x2: innerWidth,\n    y1: 0,\n    y2: 0,\n  };\n}\n",
      "type": "registry:component",
      "target": "components/charts/fade-edges.ts"
    }
  ]
}