{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "native-charts",
  "type": "registry:component",
  "title": "Native charts (React Native, beta)",
  "description": "Line and area charts for React Native and Expo, drawn with Skia and animated with Reanimated. In an Expo project the shadcn CLI installs dependencies with `npx expo install`, so native libraries get SDK-matched versions.",
  "dependencies": [
    "@shopify/react-native-skia",
    "react-native-reanimated",
    "react-native-gesture-handler",
    "react-native-worklets",
    "d3-array",
    "d3-scale",
    "d3-shape",
    "@types/d3-array",
    "@types/d3-scale",
    "@types/d3-shape"
  ],
  "files": [
    {
      "path": "../native/src/charts/area-chart.tsx",
      "content": "import {\n  TimeSeriesChart,\n  type TimeSeriesChartProps,\n} from \"./time-series-chart\";\n\nexport type AreaChartProps = TimeSeriesChartProps;\n\n/** Compose with `<Area>`, `<Grid>`, `<XAxis>`, `<ChartTooltip>`, segments. */\nexport function AreaChart(props: AreaChartProps) {\n  return <TimeSeriesChart {...props} />;\n}\n",
      "type": "registry:component",
      "target": "components/charts/area-chart.tsx"
    },
    {
      "path": "../native/src/charts/area.tsx",
      "content": "import { Group, LinearGradient, Path, vec } from \"@shopify/react-native-skia\";\nimport { type CurveFactory, curveMonotoneX } from \"d3-shape\";\nimport { useMemo } from \"react\";\nimport { collectValues, useChart, withChartPlugin } from \"./chart-context\";\nimport { DATA_TWEEN_MS } from \"./motion\";\nimport {\n  areaBuilder,\n  type FadeEdges,\n  HighlightBand,\n  lineBuilder,\n  SeriesLayer,\n  useDimOpacity,\n  useKeyedMorph,\n  useSeriesPoints,\n} from \"./series-paint\";\nimport { withAlpha } from \"./theme\";\n\nexport interface AreaProps {\n  dataKey: string;\n  /** Tooltip label. Default: `dataKey`. */\n  label?: string;\n  /** Fill colour (gradient start). Default: theme primary / secondary. */\n  fill?: string;\n  /** Gradient start opacity. Default: 0.4 (matches web). */\n  fillOpacity?: number;\n  /** Line colour. Default: same as `fill`. */\n  stroke?: string;\n  /** Default: 2 (matches web). */\n  strokeWidth?: number;\n  /** Default: curveMonotoneX (matches web `Area`). */\n  curve?: CurveFactory;\n  /** Fade fill and stroke toward the left/right edges. Default: true. */\n  fadeEdges?: FadeEdges;\n  /** Dim the series and re-stroke a band around the scrubbed point. Default: true. */\n  showHighlight?: boolean;\n  /** Morph duration in ms when `data` changes. Default: 500 (web y-domain tween). */\n  morphDuration?: number;\n}\n\nfunction AreaImpl({\n  dataKey,\n  fill,\n  fillOpacity = 0.4,\n  stroke,\n  strokeWidth = 2,\n  curve = curveMonotoneX,\n  fadeEdges = true,\n  showHighlight = true,\n  morphDuration = DATA_TWEEN_MS,\n}: AreaProps) {\n  const { yScale, series, height, margin } = useChart();\n  const seriesColor =\n    series.find((s) => s.dataKey === dataKey)?.stroke ?? \"#6366f1\";\n  const fillColor = fill ?? seriesColor;\n  const strokeColor = stroke ?? fillColor;\n  const baseline = yScale.range()[0] ?? height - margin.bottom;\n\n  const points = useSeriesPoints(dataKey, baseline);\n  const buildArea = useMemo(() => areaBuilder(curve), [curve]);\n  const buildLine = useMemo(() => lineBuilder(curve), [curve]);\n  const areaPath = useKeyedMorph(points, buildArea, morphDuration);\n  const linePath = useKeyedMorph(points, buildLine, morphDuration);\n  const opacity = useDimOpacity(showHighlight);\n\n  const stroked = (\n    <Path\n      color={strokeColor}\n      path={linePath}\n      strokeCap=\"round\"\n      strokeJoin=\"round\"\n      strokeWidth={strokeWidth}\n      style=\"stroke\"\n    />\n  );\n\n  return (\n    <SeriesLayer fadeEdges={fadeEdges}>\n      <Group opacity={opacity}>\n        <Path path={areaPath}>\n          <LinearGradient\n            colors={[\n              withAlpha(fillColor, fillOpacity),\n              withAlpha(fillColor, 0),\n            ]}\n            end={vec(0, baseline)}\n            start={vec(0, margin.top)}\n          />\n        </Path>\n        {stroked}\n      </Group>\n      {showHighlight ? <HighlightBand>{stroked}</HighlightBand> : null}\n    </SeriesLayer>\n  );\n}\n\nexport const Area = withChartPlugin(AreaImpl, {\n  series: (props, index, theme) => ({\n    dataKey: props.dataKey,\n    label: props.label ?? props.dataKey,\n    stroke:\n      props.stroke ??\n      props.fill ??\n      (index === 0 ? theme.linePrimary : theme.lineSecondary),\n    strokeWidth: props.strokeWidth ?? 2,\n  }),\n  yValues: (props, data) => collectValues(data, [props.dataKey]),\n});\n",
      "type": "registry:component",
      "target": "components/charts/area.tsx"
    },
    {
      "path": "../native/src/charts/background.tsx",
      "content": "import { Group } from \"@shopify/react-native-skia\";\nimport { useChart } from \"./chart-context\";\nimport { EdgeFade, useEnterOpacity, usePlotRect } from \"./fade-mask\";\nimport { PatternFill, type PatternPreset } from \"./patterns\";\n\nexport interface BackgroundProps {\n  /** Default: \"dots\" (homepage). */\n  pattern?: PatternPreset;\n  /** Default: theme grid. */\n  color?: string;\n  opacity?: number;\n  scale?: number;\n  strokeWidth?: number;\n  /** Left/right fade, % of plot width per edge. Default: 10. */\n  fadeHorizontalLength?: number;\n  /** Top/bottom fade, % of plot height per edge. Default: 10. */\n  fadeVerticalLength?: number;\n}\n\n/** Patterned plot backdrop that fades at the edges (web `Background`). */\nexport function Background({\n  pattern = \"dots\",\n  color,\n  opacity = 1,\n  scale = 1,\n  strokeWidth = 1,\n  fadeHorizontalLength = 10,\n  fadeVerticalLength = 10,\n}: BackgroundProps) {\n  const { theme } = useChart();\n  const rect = usePlotRect();\n  const enter = useEnterOpacity();\n  return (\n    <Group opacity={enter}>\n      <EdgeFade\n        bottom={fadeVerticalLength}\n        left={fadeHorizontalLength}\n        rect={rect}\n        right={fadeHorizontalLength}\n        top={fadeVerticalLength}\n      >\n        <PatternFill\n          color={color ?? theme.grid}\n          opacity={opacity}\n          preset={pattern}\n          rect={rect}\n          scale={scale}\n          strokeWidth={strokeWidth}\n        />\n      </EdgeFade>\n    </Group>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/charts/background.tsx"
    },
    {
      "path": "../native/src/charts/band.tsx",
      "content": "import {\n  DashPathEffect,\n  Group,\n  LinearGradient,\n  Path,\n  vec,\n} from \"@shopify/react-native-skia\";\nimport { type CurveFactory, curveNatural } from \"d3-shape\";\nimport { useMemo } from \"react\";\nimport { collectValues, useChart, withChartPlugin } from \"./chart-context\";\nimport { DATA_TWEEN_MS } from \"./motion\";\nimport {\n  areaBuilder,\n  type FadeEdges,\n  lineBuilder,\n  SeriesLayer,\n  useKeyedMorph,\n  useSeriesPoints,\n} from \"./series-paint\";\nimport { withAlpha } from \"./theme\";\n\nexport interface BandProps {\n  lowerKey: string;\n  upperKey: string;\n  label?: string;\n  /** Default: theme primary. */\n  fill?: string;\n  /** \"gradient\" fades to `gradientToOpacity` at the band's bottom. Default: \"solid\". */\n  fillType?: \"solid\" | \"gradient\";\n  /** Default: 0.15 (web). */\n  fillOpacity?: number;\n  gradientToOpacity?: number;\n  /** Edge stroke colour. Default: `fill`. */\n  stroke?: string;\n  strokeOpacity?: number;\n  strokeWidth?: number;\n  /** e.g. `[3, 4]` (recipe). Omit for solid edges. */\n  strokeDasharray?: number[];\n  curve?: CurveFactory;\n  /** Default: true (15%). */\n  fadeEdges?: FadeEdges;\n}\n\n/** Range band between two keys with optional dashed edges (web `Band`). */\nfunction BandImpl({\n  lowerKey,\n  upperKey,\n  fill,\n  fillType = \"solid\",\n  fillOpacity = 0.15,\n  gradientToOpacity = 0,\n  stroke,\n  strokeOpacity = 1,\n  strokeWidth = 1,\n  strokeDasharray,\n  curve = curveNatural,\n  fadeEdges = true,\n}: BandProps) {\n  const { theme } = useChart();\n  const color = fill ?? theme.linePrimary;\n  const edgeColor = stroke ?? color;\n\n  const points = useSeriesPoints(lowerKey, upperKey);\n  const { top, bottom } = useMemo(() => {\n    const ys = points.points.flatMap((p) => [p.y, p.y0 ?? p.y]);\n    return { top: Math.min(...ys), bottom: Math.max(...ys) };\n  }, [points]);\n  const buildArea = useMemo(() => areaBuilder(curve), [curve]);\n  const buildLower = useMemo(() => lineBuilder(curve, \"y\"), [curve]);\n  const buildUpper = useMemo(() => lineBuilder(curve, \"y0\"), [curve]);\n  const area = useKeyedMorph(points, buildArea, DATA_TWEEN_MS);\n  const lower = useKeyedMorph(points, buildLower, DATA_TWEEN_MS);\n  const upper = useKeyedMorph(points, buildUpper, DATA_TWEEN_MS);\n  const dash = strokeDasharray ? (\n    <DashPathEffect intervals={strokeDasharray} />\n  ) : null;\n\n  return (\n    <SeriesLayer fadeEdges={fadeEdges}>\n      {fillType === \"gradient\" ? (\n        <Path path={area}>\n          <LinearGradient\n            colors={[\n              withAlpha(color, fillOpacity),\n              withAlpha(color, gradientToOpacity),\n            ]}\n            end={vec(0, Number.isFinite(bottom) ? bottom : 0)}\n            start={vec(0, Number.isFinite(top) ? top : 0)}\n          />\n        </Path>\n      ) : (\n        <Path color={withAlpha(color, fillOpacity)} path={area} />\n      )}\n      <Group opacity={strokeOpacity}>\n        {[lower, upper].map((p, i) => (\n          <Path\n            color={edgeColor}\n            key={i}\n            path={p}\n            strokeCap=\"round\"\n            strokeWidth={strokeWidth}\n            style=\"stroke\"\n          >\n            {dash}\n          </Path>\n        ))}\n      </Group>\n    </SeriesLayer>\n  );\n}\n\nexport const Band = withChartPlugin(BandImpl, {\n  yValues: (props, data) =>\n    collectValues(data, [props.lowerKey, props.upperKey]),\n});\n",
      "type": "registry:component",
      "target": "components/charts/band.tsx"
    },
    {
      "path": "../native/src/charts/chart-context.tsx",
      "content": "import type { SkFont } from \"@shopify/react-native-skia\";\nimport type { ScaleLinear, ScaleTime } from \"d3-scale\";\nimport { createContext, type ReactNode, useContext } from \"react\";\nimport type { SharedValue } from \"react-native-reanimated\";\nimport type { ChartSprings, ChartTheme } from \"./theme\";\n\nexport interface Margin {\n  top: number;\n  right: number;\n  bottom: number;\n  left: number;\n}\n\nexport interface ChartSeries {\n  dataKey: string;\n  label: string;\n  stroke: string;\n  strokeWidth: number;\n  /** Colour tooltip dot/row by the value's sign (profit / loss). */\n  signColors?: { positive: string; negative: string };\n}\n\n/**\n * How a chart child feeds the shell: tooltip series and the values the\n * scales must include. Attached to components with `withChartPlugin`.\n */\nexport interface ChartPlugin<P = Record<string, unknown>> {\n  series?: (props: P, index: number, theme: ChartTheme) => ChartSeries | null;\n  yValues?: (props: P, data: Record<string, unknown>[]) => number[];\n  xValues?: (props: P, data: Record<string, unknown>[]) => Date[];\n}\n\nexport function withChartPlugin<P>(\n  component: (props: P) => ReactNode,\n  plugin: ChartPlugin<P>\n): ((props: P) => ReactNode) & { chartPlugin: ChartPlugin<P> } {\n  return Object.assign(component, { chartPlugin: plugin });\n}\n\nexport function getChartPlugin(type: unknown): ChartPlugin | undefined {\n  return (type as { chartPlugin?: ChartPlugin } | null)?.chartPlugin;\n}\n\nexport interface ChartFonts {\n  axis: SkFont;\n  tooltip: SkFont;\n  tooltipStrong: SkFont;\n  ticker: SkFont;\n  badge: SkFont;\n}\n\nexport interface ChartContextValue {\n  data: Record<string, unknown>[];\n  xDataKey: string;\n  width: number;\n  height: number;\n  margin: Margin;\n  xScale: ScaleTime<number, number>;\n  yScale: ScaleLinear<number, number>;\n  series: ChartSeries[];\n  /** Pixel x of each datum, in data order. */\n  xs: number[];\n  theme: ChartTheme;\n  fonts: ChartFonts;\n  springs: ChartSprings;\n  animationDuration: number;\n  reducedMotion: boolean;\n  /** 0 → 1 left-to-right reveal on mount. */\n  revealProgress: SharedValue<number>;\n\n  /** Index under the finger (last one, after release). */\n  activeIndex: SharedValue<number>;\n  /** Crosshair x in px, springs between snapped indices. */\n  crosshairX: SharedValue<number>;\n  /** 0 → 1 tooltip visibility. */\n  tooltipOpacity: SharedValue<number>;\n  /** Floating panel x in px (its own, softer spring). */\n  boxX: SharedValue<number>;\n  /** Measured tooltip panel width, written by ChartTooltip. */\n  boxWidth: SharedValue<number>;\n  /** Current date ticker pill width, written by DateTicker. */\n  tickerWidth: SharedValue<number>;\n  /** Hover-highlight band, in px. */\n  highlightX: SharedValue<number>;\n  highlightWidth: SharedValue<number>;\n\n  /** Two-finger segment selection, in px / data index. */\n  selectionStartX: SharedValue<number>;\n  selectionEndX: SharedValue<number>;\n  selectionStartIndex: SharedValue<number>;\n  selectionEndIndex: SharedValue<number>;\n  /** 0 → 1 selection visibility. */\n  selectionOpacity: SharedValue<number>;\n\n  /** Panel fade (web: 100ms). */\n  panelOpacity: SharedValue<number>;\n  /** 0 → 1 panel entrance spring (scale 0.85 → 1, x ±20 → 0). */\n  panelEntrance: SharedValue<number>;\n  /** 1 when the panel sits left of the crosshair. */\n  panelFlipped: SharedValue<number>;\n  /** 0 → 1 dot/halo grow-in spring. */\n  dotEntrance: SharedValue<number>;\n  /** Series dim + highlight band visibility (web: 0.4s easeInOut). */\n  highlightOpacity: SharedValue<number>;\n}\n\nexport const ChartContext = createContext<ChartContextValue | null>(null);\n\nexport function useChart(): ChartContextValue {\n  const ctx = useContext(ChartContext);\n  if (!ctx) {\n    throw new Error(\n      \"Chart components must be rendered inside <LineChart> or <AreaChart>.\"\n    );\n  }\n  return ctx;\n}\n\nexport function getDate(datum: Record<string, unknown>, key: string): Date {\n  const value = datum[key];\n  return value instanceof Date ? value : new Date(value as string | number);\n}\n\nexport function getNumber(datum: Record<string, unknown>, key: string): number {\n  const value = Number(datum[key]);\n  return Number.isFinite(value) ? value : 0;\n}\n\n/** The value, or null when missing (e.g. actuals on forecast rows). */\nexport function getValue(\n  datum: Record<string, unknown>,\n  key: string\n): number | null {\n  const raw = datum[key];\n  if (raw === null || raw === undefined || raw === \"\") {\n    return null;\n  }\n  const value = Number(raw);\n  return Number.isFinite(value) ? value : null;\n}\n\n/** Finite values of `keys` across `data`. */\nexport function collectValues(\n  data: Record<string, unknown>[],\n  keys: string[]\n): number[] {\n  const out: number[] = [];\n  for (const d of data) {\n    for (const key of keys) {\n      const v = getValue(d, key);\n      if (v !== null) {\n        out.push(v);\n      }\n    }\n  }\n  return out;\n}\n\nexport function nearestIndex(xs: number[], x: number): number {\n  \"worklet\";\n  const last = xs.length - 1;\n  if (last <= 0 || x <= xs[0]) {\n    return 0;\n  }\n  if (x >= xs[last]) {\n    return last;\n  }\n  let lo = 0;\n  let hi = last;\n  while (hi - lo > 1) {\n    const mid = Math.floor((lo + hi) / 2);\n    if (xs[mid] < x) {\n      lo = mid;\n    } else {\n      hi = mid;\n    }\n  }\n  return x - xs[lo] < xs[hi] - x ? lo : hi;\n}\n\n/**\n * y at pixel `x`, linear between neighbouring defined points; NaN where the\n * series has no value (so dots can hide on forecast rows).\n */\nexport function sampleY(xs: number[], ys: number[], x: number): number {\n  \"worklet\";\n  const n = xs.length;\n  if (n === 0) {\n    return Number.NaN;\n  }\n  const i = nearestIndex(xs, x);\n  const yi = ys[i] ?? Number.NaN;\n  if (!Number.isFinite(yi)) {\n    return Number.NaN;\n  }\n  const j = x >= (xs[i] ?? 0) ? i + 1 : i - 1;\n  const yj = ys[j];\n  const xi = xs[i] ?? 0;\n  const xj = xs[j];\n  if (\n    yj === undefined ||\n    xj === undefined ||\n    !Number.isFinite(yj) ||\n    xj === xi\n  ) {\n    return yi;\n  }\n  const t = (x - xi) / (xj - xi);\n  return yi + (yj - yi) * t;\n}\n",
      "type": "registry:component",
      "target": "components/charts/chart-context.tsx"
    },
    {
      "path": "../native/src/charts/chart-tooltip.tsx",
      "content": "import {\n  Circle,\n  DashPathEffect,\n  Group,\n  Line,\n  LinearGradient,\n  Rect,\n  RoundedRect,\n  rect,\n  rrect,\n  Shadow,\n  type SkFont,\n  Text,\n  vec,\n} from \"@shopify/react-native-skia\";\nimport { useEffect, useMemo } from \"react\";\nimport {\n  type SharedValue,\n  useAnimatedReaction,\n  useDerivedValue,\n  useSharedValue,\n  withSpring,\n} from \"react-native-reanimated\";\nimport {\n  type ChartSeries,\n  getDate,\n  getValue,\n  sampleY,\n  useChart,\n} from \"./chart-context\";\nimport { DateTicker } from \"./date-ticker\";\nimport { type ChartMarker, markerIndex } from \"./markers\";\nimport {\n  DOT_HALO_OPACITY,\n  DOT_HALO_SCALE,\n  DOT_SIZE,\n  PANEL_ENTRANCE_SCALE,\n  PANEL_ENTRANCE_X,\n} from \"./motion\";\nimport { withAlpha } from \"./theme\";\n\nexport type IndicatorFadeEdges = \"both\" | \"top\" | \"bottom\" | \"none\";\n\nexport interface ChartTooltipProps {\n  formatLabel?: (date: Date) => string;\n  formatValue?: (value: number) => string;\n  showCrosshair?: boolean;\n  showDots?: boolean;\n  /** Rolling month/day pill on the x-axis. Default: true. */\n  showDatePill?: boolean;\n  /**\n   * Pill label. Split on the first space into two rolling stacks\n   * (\"Jan 27\" → \"Jan\" + \"27\"); one word (\"Wednesday\") rolls as one.\n   */\n  datePillFormat?: (date: Date) => string;\n  /** Floating panel with the header and rows. Default: true. */\n  showPanel?: boolean;\n  /** Crosshair dash pattern, e.g. `[4, 4]`. Omit for the gradient line. */\n  indicatorDasharray?: [number, number];\n  /** Vertical crosshair fade (ignored when dashed). Default: \"both\". */\n  indicatorFadeEdges?: IndicatorFadeEdges;\n  /** Crosshair colour. Default: theme crosshair. */\n  indicatorColor?: string;\n  /** Colour the crosshair like this series' dot (e.g. profit / loss sign). */\n  indicatorColorFrom?: string;\n  /** Add marker rows (title + description) on marker days. Max 2, then \"+N more\". */\n  markers?: ChartMarker[];\n}\n\n// Panel metrics (web TooltipContent: px-3.5, header py-2.5, rows gap-2).\nconst PAD_X = 12;\nconst HEADER_H = 32;\nconst BODY_PAD_Y = 10;\nconst ROW_H = 18;\nconst ROW_GAP = 6;\nconst ROW_DOT = 8;\nconst LABEL_GAP = 8;\nconst VALUE_GAP = 24;\nconst MIN_WIDTH = 150;\nconst RADIUS = 12;\nconst MARKER_SECTION_GAP = 8;\nconst MARKER_ROW_H = 36;\nconst MARKER_MORE_H = 16;\nconst MAX_MARKER_ROWS = 2;\n/** Web PANEL_SIZE_SPRING: critically damped, hugs new content. */\nconst PANEL_SIZE_SPRING = { stiffness: 500, damping: 45, mass: 1 };\n/** Indicator fade zone, % of its height (web default 10). */\nconst INDICATOR_FADE = 0.1;\n\nconst defaultLabel = (date: Date) =>\n  date.toLocaleDateString(\"en-US\", {\n    weekday: \"short\",\n    month: \"short\",\n    day: \"numeric\",\n  });\nconst defaultValue = (value: number) => value.toLocaleString(\"en-US\");\n\n/**\n * Crosshair, series dots, date ticker and floating panel, all drawn in Skia\n * and driven by springs on the UI thread, so scrubbing never re-renders React.\n */\nexport function ChartTooltip({\n  formatLabel = defaultLabel,\n  formatValue = defaultValue,\n  showCrosshair = true,\n  showDots = true,\n  showDatePill = true,\n  datePillFormat,\n  showPanel = true,\n  indicatorDasharray,\n  indicatorFadeEdges = \"both\",\n  indicatorColor,\n  indicatorColorFrom,\n  markers,\n}: ChartTooltipProps) {\n  const {\n    data,\n    xDataKey,\n    height,\n    margin,\n    xs,\n    yScale,\n    series,\n    theme,\n    fonts,\n    activeIndex,\n    crosshairX,\n    tooltipOpacity,\n    boxX,\n    boxWidth,\n    panelOpacity,\n    panelEntrance,\n    panelFlipped,\n    dotEntrance,\n  } = useChart();\n\n  const titles = useMemo(\n    () => data.map((d) => formatLabel(getDate(d, xDataKey))),\n    [data, xDataKey, formatLabel]\n  );\n\n  const rows = useMemo(\n    () =>\n      series.map((s) => {\n        const raw = data.map((d) => getValue(d, s.dataKey));\n        const values = raw.map((v) => (v === null ? \"—\" : formatValue(v)));\n        const sign = s.signColors;\n        return {\n          series: s,\n          values,\n          valueWidths: values.map((v) => fonts.tooltipStrong.getTextWidth(v)),\n          labelWidth: fonts.tooltip.getTextWidth(s.label),\n          ys: raw.map((v) => (v === null ? Number.NaN : yScale(v))),\n          colors: raw.map((v) => {\n            if (!sign) {\n              return s.stroke;\n            }\n            return (v ?? 0) >= 0 ? sign.positive : sign.negative;\n          }),\n        };\n      }),\n    [series, data, formatValue, fonts, yScale]\n  );\n\n  // Per data index: up to two markers, plus \"+N more\".\n  const markerRows = useMemo(() => {\n    const slots = Array.from({ length: MAX_MARKER_ROWS }, () => ({\n      titles: [] as string[],\n      descriptions: [] as string[],\n      colors: [] as string[],\n    }));\n    const more: string[] = [];\n    const extra: number[] = [];\n    let widest = 0;\n    data.forEach((d, i) => {\n      const hits = (markers ?? []).filter(\n        (m) => markerIndex([d], xDataKey, m.date) === 0\n      );\n      slots.forEach((slot, k) => {\n        const m = hits[k];\n        slot.titles[i] = m?.title ?? \"\";\n        slot.descriptions[i] = m?.description ?? \"\";\n        slot.colors[i] = m?.color ?? theme.foregroundMuted;\n        if (m) {\n          widest = Math.max(\n            widest,\n            fonts.tooltipStrong.getTextWidth(m.title),\n            fonts.tooltip.getTextWidth(m.description ?? \"\")\n          );\n        }\n      });\n      const overflow = hits.length - MAX_MARKER_ROWS;\n      more[i] = overflow > 0 ? `+${overflow} more…` : \"\";\n      const shown = Math.min(MAX_MARKER_ROWS, hits.length);\n      extra[i] =\n        shown > 0\n          ? MARKER_SECTION_GAP +\n            shown * MARKER_ROW_H +\n            (overflow > 0 ? MARKER_MORE_H : 0)\n          : 0;\n    });\n    return { slots, more, extra, widest };\n  }, [data, xDataKey, markers, fonts, theme.foregroundMuted]);\n\n  const panelWidth = useMemo(() => {\n    const titleW = Math.max(\n      0,\n      ...titles.map((t) => fonts.tooltipStrong.getTextWidth(t))\n    );\n    const rowW = Math.max(\n      0,\n      ...rows.map(\n        (r) =>\n          ROW_DOT +\n          LABEL_GAP +\n          r.labelWidth +\n          VALUE_GAP +\n          Math.max(0, ...r.valueWidths)\n      )\n    );\n    const markerW = markerRows.widest\n      ? markerRows.widest + ROW_DOT + LABEL_GAP\n      : 0;\n    return Math.ceil(\n      Math.max(\n        MIN_WIDTH,\n        titleW + PAD_X * 2,\n        rowW + PAD_X * 2,\n        markerW + PAD_X * 2\n      )\n    );\n  }, [titles, rows, fonts, markerRows.widest]);\n\n  useEffect(() => {\n    boxWidth.value = showPanel ? panelWidth : 0;\n  }, [boxWidth, panelWidth, showPanel]);\n\n  const bodyBottom =\n    HEADER_H + BODY_PAD_Y + rows.length * ROW_H + (rows.length - 1) * ROW_GAP;\n  const baseHeight = bodyBottom + BODY_PAD_Y;\n  // Grows for marker rows on marker days (web panel size spring).\n  const panelHeight = useSharedValue(baseHeight);\n  const extra = markerRows.extra;\n  useAnimatedReaction(\n    () => activeIndex.value,\n    (i) => {\n      const target = baseHeight + (extra[i] ?? 0);\n      panelHeight.value =\n        panelOpacity.value === 0\n          ? target\n          : withSpring(target, PANEL_SIZE_SPRING);\n    }\n  );\n  const borderHeight = useDerivedValue(() => panelHeight.value - 1);\n  const panelClip = useDerivedValue(() =>\n    rrect(rect(0, 0, panelWidth, panelHeight.value), RADIUS, RADIUS)\n  );\n  const crosshairColors = useMemo(\n    () => rows.find((r) => r.series.dataKey === indicatorColorFrom)?.colors,\n    [rows, indicatorColorFrom]\n  );\n  const top = margin.top;\n  const bottom = height - margin.bottom;\n\n  const title = useDerivedValue(() => titles[activeIndex.value] ?? \"\");\n  // Web entrance: scale 0.85 → 1 and x ∓20 → 0 from the crosshair-side top\n  // corner, on PANEL_ENTRANCE_SPRING. Origin flips with the panel.\n  const panelTransform = useDerivedValue(() => {\n    const e = panelEntrance.value;\n    const flipped = panelFlipped.value === 1;\n    const originX = flipped ? panelWidth : 0;\n    const slide = (flipped ? PANEL_ENTRANCE_X : -PANEL_ENTRANCE_X) * (1 - e);\n    const scale = PANEL_ENTRANCE_SCALE + (1 - PANEL_ENTRANCE_SCALE) * e;\n    return [\n      { translateX: boxX.value + slide + originX },\n      { translateY: top },\n      { scale },\n      { translateX: -originX },\n    ];\n  });\n\n  return (\n    <>\n      {showCrosshair ? (\n        <Crosshair\n          activeIndex={activeIndex}\n          bottom={bottom}\n          color={indicatorColor ?? theme.crosshair}\n          colors={crosshairColors}\n          crosshairX={crosshairX}\n          dash={indicatorDasharray}\n          fade={indicatorFadeEdges}\n          opacity={tooltipOpacity}\n          top={top}\n        />\n      ) : null}\n      {showDots\n        ? rows.map((r) => (\n            <TooltipDot\n              activeIndex={activeIndex}\n              colors={r.colors}\n              crosshairX={crosshairX}\n              entrance={dotEntrance}\n              key={r.series.dataKey}\n              opacity={tooltipOpacity}\n              xs={xs}\n              ys={r.ys}\n            />\n          ))\n        : null}\n      {showDatePill ? <DateTicker formatLabel={datePillFormat} /> : null}\n      {showPanel ? (\n        <Group opacity={panelOpacity} transform={panelTransform}>\n          <Group>\n            <RoundedRect\n              color={theme.tooltipBackground}\n              height={panelHeight}\n              r={RADIUS}\n              width={panelWidth}\n              x={0}\n              y={0}\n            >\n              <Shadow blur={12} color=\"rgba(0,0,0,0.10)\" dx={0} dy={4} />\n            </RoundedRect>\n            <RoundedRect\n              color={theme.tooltipBorder}\n              height={borderHeight}\n              r={RADIUS - 0.5}\n              strokeWidth={1}\n              style=\"stroke\"\n              width={panelWidth - 1}\n              x={0.5}\n              y={0.5}\n            />\n            <Text\n              color={theme.tooltipForeground}\n              font={fonts.tooltipStrong}\n              text={title}\n              x={PAD_X}\n              y={HEADER_H / 2 + 4.5}\n            />\n            <Rect\n              color={theme.tooltipBorder}\n              height={1}\n              width={panelWidth}\n              x={0}\n              y={HEADER_H}\n            />\n            {rows.map((r, i) => (\n              <TooltipRow\n                activeIndex={activeIndex}\n                colors={r.colors}\n                key={r.series.dataKey}\n                labelColor={theme.tooltipMuted}\n                labelFont={fonts.tooltip}\n                panelWidth={panelWidth}\n                series={r.series}\n                valueColor={theme.tooltipForeground}\n                valueFont={fonts.tooltipStrong}\n                values={r.values}\n                valueWidths={r.valueWidths}\n                y={HEADER_H + BODY_PAD_Y + i * (ROW_H + ROW_GAP)}\n              />\n            ))}\n            {markers?.length ? (\n              <Group clip={panelClip}>\n                <MarkerSection\n                  activeIndex={activeIndex}\n                  divider={theme.tooltipBorder}\n                  fonts={fonts}\n                  more={markerRows.more}\n                  muted={theme.tooltipMuted}\n                  panelWidth={panelWidth}\n                  slots={markerRows.slots}\n                  strong={theme.tooltipForeground}\n                  top={bodyBottom + BODY_PAD_Y}\n                />\n              </Group>\n            ) : null}\n          </Group>\n        </Group>\n      ) : null}\n    </>\n  );\n}\n\nfunction MarkerSection({\n  slots,\n  more,\n  activeIndex,\n  top,\n  panelWidth,\n  fonts,\n  divider,\n  strong,\n  muted,\n}: {\n  slots: { titles: string[]; descriptions: string[]; colors: string[] }[];\n  more: string[];\n  activeIndex: SharedValue<number>;\n  top: number;\n  panelWidth: number;\n  fonts: ReturnType<typeof useChart>[\"fonts\"];\n  divider: string;\n  strong: string;\n  muted: string;\n}) {\n  const visible = useDerivedValue(() =>\n    slots[0]?.titles[activeIndex.value] ? 1 : 0\n  );\n  const moreText = useDerivedValue(() => more[activeIndex.value] ?? \"\");\n  const moreY = useDerivedValue(() => {\n    const shown = slots.filter((s) => s.titles[activeIndex.value]).length;\n    return top + MARKER_SECTION_GAP + shown * MARKER_ROW_H + 11;\n  });\n  return (\n    <Group opacity={visible}>\n      <Rect\n        color={divider}\n        height={1}\n        width={panelWidth - PAD_X * 2}\n        x={PAD_X}\n        y={top}\n      />\n      {slots.map((slot, k) => (\n        <MarkerRow\n          activeIndex={activeIndex}\n          fonts={fonts}\n          key={k}\n          muted={muted}\n          slot={slot}\n          strong={strong}\n          y={top + MARKER_SECTION_GAP + k * MARKER_ROW_H}\n        />\n      ))}\n      <Text\n        color={muted}\n        font={fonts.tooltip}\n        text={moreText}\n        x={PAD_X}\n        y={moreY}\n      />\n    </Group>\n  );\n}\n\nfunction MarkerRow({\n  slot,\n  activeIndex,\n  y,\n  fonts,\n  strong,\n  muted,\n}: {\n  slot: { titles: string[]; descriptions: string[]; colors: string[] };\n  activeIndex: SharedValue<number>;\n  y: number;\n  fonts: ReturnType<typeof useChart>[\"fonts\"];\n  strong: string;\n  muted: string;\n}) {\n  const title = useDerivedValue(() => slot.titles[activeIndex.value] ?? \"\");\n  const description = useDerivedValue(\n    () => slot.descriptions[activeIndex.value] ?? \"\"\n  );\n  const color = useDerivedValue(() => slot.colors[activeIndex.value] ?? muted);\n  const visible = useDerivedValue(() => (title.value ? 1 : 0));\n  return (\n    <Group opacity={visible}>\n      <Circle\n        color={color}\n        cx={PAD_X + ROW_DOT / 2}\n        cy={y + 9}\n        r={ROW_DOT / 2}\n      />\n      <Text\n        color={strong}\n        font={fonts.tooltipStrong}\n        text={title}\n        x={PAD_X + ROW_DOT + LABEL_GAP}\n        y={y + 13}\n      />\n      <Text\n        color={muted}\n        font={fonts.tooltip}\n        text={description}\n        x={PAD_X + ROW_DOT + LABEL_GAP}\n        y={y + 30}\n      />\n    </Group>\n  );\n}\n\nfunction fadeStops(fade: IndicatorFadeEdges, color: string) {\n  const clear = withAlpha(color, 0);\n  const f = INDICATOR_FADE;\n  switch (fade) {\n    case \"top\":\n      return { colors: [clear, color, color], positions: [0, f, 1] };\n    case \"bottom\":\n      return { colors: [color, color, clear], positions: [0, 1 - f, 1] };\n    case \"none\":\n      return { colors: [color, color], positions: [0, 1] };\n    default:\n      return {\n        colors: [clear, color, color, clear],\n        positions: [0, f, 1 - f, 1],\n      };\n  }\n}\n\nfunction Crosshair({\n  activeIndex,\n  colors,\n  crosshairX,\n  opacity,\n  top,\n  bottom,\n  color,\n  dash,\n  fade,\n}: {\n  crosshairX: SharedValue<number>;\n  opacity: SharedValue<number>;\n  top: number;\n  bottom: number;\n  color: string;\n  dash?: [number, number];\n  fade: IndicatorFadeEdges;\n  activeIndex: SharedValue<number>;\n  colors?: string[];\n}) {\n  const live = useDerivedValue(\n    () => (colors ? colors[activeIndex.value] : undefined) ?? color\n  );\n  const p1 = useDerivedValue(() => ({ x: crosshairX.value, y: top }));\n  const p2 = useDerivedValue(() => ({ x: crosshairX.value, y: bottom }));\n  const barX = useDerivedValue(() => crosshairX.value - 0.5);\n\n  // Per-point colour (e.g. profit / loss) draws a plain line.\n  if (dash || colors) {\n    return (\n      <Line color={live} opacity={opacity} p1={p1} p2={p2} strokeWidth={1}>\n        {dash ? <DashPathEffect intervals={dash} /> : null}\n      </Line>\n    );\n  }\n\n  const stops = fadeStops(fade, color);\n  return (\n    <Rect height={bottom - top} opacity={opacity} width={1} x={barX} y={top}>\n      <LinearGradient\n        colors={stops.colors}\n        end={vec(0, bottom)}\n        positions={stops.positions}\n        start={vec(0, top)}\n      />\n    </Rect>\n  );\n}\n\nfunction TooltipDot({\n  xs,\n  ys,\n  colors,\n  activeIndex,\n  crosshairX,\n  opacity,\n  entrance,\n}: {\n  xs: number[];\n  ys: number[];\n  colors: string[];\n  activeIndex: SharedValue<number>;\n  crosshairX: SharedValue<number>;\n  opacity: SharedValue<number>;\n  entrance: SharedValue<number>;\n}) {\n  const halos = useMemo(\n    () => colors.map((c) => withAlpha(c, DOT_HALO_OPACITY)),\n    [colors]\n  );\n  // Rides the crosshair spring, following the line between points; hidden\n  // where the series has no value.\n  const cy = useDerivedValue(() => sampleY(xs, ys, crosshairX.value));\n  const visible = useDerivedValue(() =>\n    Number.isFinite(cy.value) ? opacity.value : 0\n  );\n  const safeCy = useDerivedValue(() =>\n    Number.isFinite(cy.value) ? cy.value : -100\n  );\n  const color = useDerivedValue(() => colors[activeIndex.value] ?? \"#000\");\n  const halo = useDerivedValue(() => halos[activeIndex.value] ?? \"#0000\");\n  // Web tooltip-dot: dot 2.5 → 5, halo 5 → 13, both on HALO_SPRING.\n  const r = useDerivedValue(() => DOT_SIZE * (0.5 + 0.5 * entrance.value));\n  const haloR = useDerivedValue(\n    () => DOT_SIZE + DOT_SIZE * (DOT_HALO_SCALE - 1) * entrance.value\n  );\n  return (\n    <Group opacity={visible}>\n      <Circle color={halo} cx={crosshairX} cy={safeCy} r={haloR} />\n      <Circle color={color} cx={crosshairX} cy={safeCy} r={r} />\n    </Group>\n  );\n}\n\nfunction TooltipRow({\n  series,\n  colors,\n  values,\n  valueWidths,\n  activeIndex,\n  panelWidth,\n  y,\n  labelFont,\n  valueFont,\n  labelColor,\n  valueColor,\n}: {\n  series: ChartSeries;\n  colors: string[];\n  values: string[];\n  valueWidths: number[];\n  activeIndex: SharedValue<number>;\n  panelWidth: number;\n  y: number;\n  labelFont: SkFont;\n  valueFont: SkFont;\n  labelColor: string;\n  valueColor: string;\n}) {\n  const value = useDerivedValue(() => values[activeIndex.value] ?? \"\");\n  const valueX = useDerivedValue(\n    () => panelWidth - PAD_X - (valueWidths[activeIndex.value] ?? 0)\n  );\n  const dotColor = useDerivedValue(\n    () => colors[activeIndex.value] ?? series.stroke\n  );\n  const baseline = y + ROW_H / 2 + 4.5;\n  return (\n    <>\n      <Circle\n        color={dotColor}\n        cx={PAD_X + ROW_DOT / 2}\n        cy={y + ROW_H / 2}\n        r={ROW_DOT / 2}\n      />\n      <Text\n        color={labelColor}\n        font={labelFont}\n        text={series.label}\n        x={PAD_X + ROW_DOT + LABEL_GAP}\n        y={baseline}\n      />\n      <Text\n        color={valueColor}\n        font={valueFont}\n        text={value}\n        x={valueX}\n        y={baseline}\n      />\n    </>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/charts/chart-tooltip.tsx"
    },
    {
      "path": "../native/src/charts/date-ticker.tsx",
      "content": "import {\n  Group,\n  RoundedRect,\n  rect,\n  type SkFont,\n  Text,\n} from \"@shopify/react-native-skia\";\nimport { useEffect, useMemo } from \"react\";\nimport {\n  useAnimatedReaction,\n  useDerivedValue,\n  useSharedValue,\n  withSpring,\n} from \"react-native-reanimated\";\nimport { getDate, useChart } from \"./chart-context\";\n\nexport interface DateTickerProps {\n  /** Must produce \"<month> <day>\" style labels; split on the first space. */\n  formatLabel?: (date: Date) => string;\n}\n\n// Web DateTicker: h-6 rows, px-4 / py-1 pill, gap-1 between stacks.\nconst ITEM_H = 24;\nconst PAD_X = 16;\nconst PAD_Y = 4;\nconst GAP = 4;\n/** Measured widths are advance widths; pad so glyph edges aren't clipped. */\nconst WIDTH_SLACK = 2;\n/** Stack clips extend this far so glyph overhang (\"y\", \"f\") isn't cut. */\nconst CLIP_BLEED = 2;\nconst PILL_H = ITEM_H + PAD_Y * 2;\n/** Above this many points, roll the whole label as one stack (web: 60). */\nconst COMPACT_THRESHOLD = 60;\n\nconst defaultFormat = (date: Date) =>\n  date.toLocaleDateString(\"en-US\", { month: \"short\", day: \"numeric\" });\n\ninterface Stack {\n  /** Unique consecutive values, e.g. [\"Sep\", \"Oct\"]. */\n  values: string[];\n  widths: number[];\n  /** Data index → stack index. */\n  indexOf: number[];\n}\n\nfunction toStack(parts: string[], font: SkFont): Stack {\n  const values: string[] = [];\n  const indexOf: number[] = [];\n  for (const part of parts) {\n    if (values.at(-1) !== part) {\n      values.push(part);\n    }\n    indexOf.push(values.length - 1);\n  }\n  return {\n    values,\n    widths: values.map((v) =>\n      v ? Math.ceil(font.getTextWidth(v)) + WIDTH_SLACK : 0\n    ),\n    indexOf,\n  };\n}\n\n/**\n * The web DateTicker: a pill that rides the crosshair along the x-axis, with\n * month and day rolling vertically like NumberFlow. Every frame runs on the\n * UI thread; React only renders when data changes.\n */\nexport function DateTicker({ formatLabel = defaultFormat }: DateTickerProps) {\n  const {\n    data,\n    xDataKey,\n    width,\n    height,\n    margin,\n    theme,\n    fonts,\n    springs,\n    reducedMotion,\n    activeIndex,\n    crosshairX,\n    tooltipOpacity,\n    tickerWidth,\n  } = useChart();\n\n  const { month, day } = useMemo(() => {\n    const labels = data.map((d) => formatLabel(getDate(d, xDataKey)));\n    const compact = labels.length > COMPACT_THRESHOLD;\n    const split = labels.map((label) => {\n      if (compact) {\n        return [label, \"\"] as const;\n      }\n      const space = label.indexOf(\" \");\n      return space < 0\n        ? ([label, \"\"] as const)\n        : ([label.slice(0, space), label.slice(space + 1)] as const);\n    });\n    return {\n      month: toStack(\n        split.map((s) => s[0]),\n        fonts.ticker\n      ),\n      day: toStack(\n        split.map((s) => s[1]),\n        fonts.ticker\n      ),\n    };\n  }, [data, xDataKey, formatLabel, fonts.ticker]);\n\n  const monthY = useSharedValue(0);\n  const dayY = useSharedValue(0);\n  const monthW = useSharedValue(month.widths[0] ?? 0);\n  const dayW = useSharedValue(day.widths[0] ?? 0);\n\n  const spring = springs.ticker;\n\n  const pillWidthFor = (m: number, d: number) => {\n    \"worklet\";\n    return PAD_X * 2 + m + (d > 0 ? GAP + d : 0);\n  };\n\n  useAnimatedReaction(\n    () => activeIndex.value,\n    (i) => {\n      const mi = month.indexOf[i] ?? 0;\n      const di = day.indexOf[i] ?? 0;\n      const mw = month.widths[mi] ?? 0;\n      const dw = day.widths[di] ?? 0;\n      // Jump on first show so the pill doesn't roll in from the last date.\n      const jump = reducedMotion || tooltipOpacity.value < 0.05;\n      if (jump) {\n        monthY.value = -mi * ITEM_H;\n        dayY.value = -di * ITEM_H;\n        monthW.value = mw;\n        dayW.value = dw;\n      } else {\n        monthY.value = withSpring(-mi * ITEM_H, spring);\n        dayY.value = withSpring(-di * ITEM_H, spring);\n        monthW.value = withSpring(mw, spring);\n        dayW.value = withSpring(dw, spring);\n      }\n      tickerWidth.value = pillWidthFor(mw, dw);\n    }\n  );\n\n  // Clear the axis-label fade when the ticker unmounts.\n  useEffect(\n    () => () => {\n      tickerWidth.value = 0;\n    },\n    [tickerWidth]\n  );\n\n  const pillWidth = useDerivedValue(() =>\n    pillWidthFor(monthW.value, dayW.value)\n  );\n  const top = height - margin.bottom / 2 - PILL_H / 2;\n\n  const pillTransform = useDerivedValue(() => {\n    const w = pillWidth.value;\n    const x = Math.min(Math.max(crosshairX.value - w / 2, 0), width - w);\n    return [{ translateX: x }, { translateY: top }];\n  });\n\n  const monthClip = useDerivedValue(() =>\n    rect(\n      PAD_X - CLIP_BLEED,\n      PAD_Y - CLIP_BLEED,\n      monthW.value + CLIP_BLEED * 2,\n      ITEM_H + CLIP_BLEED * 2\n    )\n  );\n  const dayX = useDerivedValue(\n    () => PAD_X + monthW.value + (dayW.value > 0 ? GAP : 0)\n  );\n  const dayClip = useDerivedValue(() =>\n    rect(\n      dayX.value - CLIP_BLEED,\n      PAD_Y - CLIP_BLEED,\n      dayW.value + CLIP_BLEED * 2,\n      ITEM_H + CLIP_BLEED * 2\n    )\n  );\n  const monthTransform = useDerivedValue(() => [\n    { translateX: PAD_X + monthW.value / 2 },\n    { translateY: PAD_Y + monthY.value },\n  ]);\n  const dayTransform = useDerivedValue(() => [\n    { translateX: dayX.value + dayW.value / 2 },\n    { translateY: PAD_Y + dayY.value },\n  ]);\n\n  return (\n    <Group opacity={tooltipOpacity} transform={pillTransform}>\n      <RoundedRect\n        color={theme.tickerBackground}\n        height={PILL_H}\n        r={PILL_H / 2}\n        width={pillWidth}\n        x={0}\n        y={0}\n      />\n      <Group clip={monthClip}>\n        <Group transform={monthTransform}>\n          <StackItems\n            color={theme.tickerForeground}\n            font={fonts.ticker}\n            stack={month}\n          />\n        </Group>\n      </Group>\n      <Group clip={dayClip}>\n        <Group transform={dayTransform}>\n          <StackItems\n            color={theme.tickerForeground}\n            font={fonts.ticker}\n            stack={day}\n          />\n        </Group>\n      </Group>\n    </Group>\n  );\n}\n\nfunction StackItems({\n  stack,\n  font,\n  color,\n}: {\n  stack: Stack;\n  font: SkFont;\n  color: string;\n}) {\n  const baseline = ITEM_H / 2 + font.getSize() * 0.36;\n  return (\n    <>\n      {stack.values.map((value, k) =>\n        value ? (\n          <Text\n            color={color}\n            font={font}\n            key={`${value}-${k}`}\n            text={value}\n            x={-(stack.widths[k] ?? 0) / 2}\n            y={k * ITEM_H + baseline}\n          />\n        ) : null\n      )}\n    </>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/charts/date-ticker.tsx"
    },
    {
      "path": "../native/src/charts/fade-mask.tsx",
      "content": "import {\n  Group,\n  LinearGradient,\n  Mask,\n  Rect,\n  vec,\n} from \"@shopify/react-native-skia\";\nimport { type ReactNode, useEffect } from \"react\";\nimport { useSharedValue, withTiming } from \"react-native-reanimated\";\nimport { useChart } from \"./chart-context\";\nimport { EASE_OUT, ENTER_FADE_MS } from \"./motion\";\nimport type { PatternRect } from \"./patterns\";\n\nconst CLEAR = \"rgba(0,0,0,0)\";\nconst SOLID = \"#000\";\n\nfunction stops(start: number, end: number) {\n  const a = Math.min(45, Math.max(0, start)) / 100;\n  const b = Math.min(45, Math.max(0, end)) / 100;\n  return {\n    colors: [a > 0 ? CLEAR : SOLID, SOLID, SOLID, b > 0 ? CLEAR : SOLID],\n    positions: [0, a, 1 - b, 1],\n  };\n}\n\nexport interface EdgeFadeProps {\n  rect: PatternRect;\n  /** Fade zones as % of the rect, per edge (0–45). */\n  left?: number;\n  right?: number;\n  top?: number;\n  bottom?: number;\n  children: ReactNode;\n}\n\n/** Alpha-mask children so they fade out toward the chosen edges. */\nexport function EdgeFade({\n  rect,\n  left = 0,\n  right = 0,\n  top = 0,\n  bottom = 0,\n  children,\n}: EdgeFadeProps) {\n  if (left + right + top + bottom === 0) {\n    return <>{children}</>;\n  }\n  const { x, y, width, height } = rect;\n  const h = stops(left, right);\n  const v = stops(top, bottom);\n  return (\n    <Mask\n      mask={\n        <Group>\n          <Rect height={height} width={width} x={x} y={y}>\n            <LinearGradient\n              colors={h.colors}\n              end={vec(x + width, 0)}\n              positions={h.positions}\n              start={vec(x, 0)}\n            />\n          </Rect>\n          <Rect blendMode=\"dstIn\" height={height} width={width} x={x} y={y}>\n            <LinearGradient\n              colors={v.colors}\n              end={vec(0, y + height)}\n              positions={v.positions}\n              start={vec(0, y)}\n            />\n          </Rect>\n        </Group>\n      }\n    >\n      {children}\n    </Mask>\n  );\n}\n\n/** The plot area (inside margins). */\nexport function usePlotRect(): PatternRect {\n  const { width, height, margin } = useChart();\n  return {\n    x: margin.left,\n    y: margin.top,\n    width: Math.max(0, width - margin.left - margin.right),\n    height: Math.max(0, height - margin.top - margin.bottom),\n  };\n}\n\n/** 0 → 1 over 420ms ease-out on mount, like web decorative layers. */\nexport function useEnterOpacity(duration = ENTER_FADE_MS) {\n  const { reducedMotion } = useChart();\n  const opacity = useSharedValue(reducedMotion ? 1 : 0);\n  useEffect(() => {\n    opacity.value = withTiming(1, {\n      duration: reducedMotion ? 0 : duration,\n      easing: EASE_OUT,\n    });\n  }, [opacity, reducedMotion, duration]);\n  return opacity;\n}\n",
      "type": "registry:component",
      "target": "components/charts/fade-mask.tsx"
    },
    {
      "path": "../native/src/charts/grid.tsx",
      "content": "import {\n  DashPathEffect,\n  LinearGradient,\n  Path,\n  Skia,\n  vec,\n} from \"@shopify/react-native-skia\";\nimport { useMemo } from \"react\";\nimport { useChart } from \"./chart-context\";\n\nexport interface GridProps {\n  horizontal?: boolean;\n  vertical?: boolean;\n  numTicksRows?: number;\n  numTicksColumns?: number;\n  stroke?: string;\n  strokeWidth?: number;\n  /** Default: [4, 4] (matches web \"4,4\"). Pass `null` for solid lines. */\n  strokeDasharray?: [number, number] | null;\n  /** Fade horizontal lines out at the left and right edges. Default: true. */\n  fadeHorizontal?: boolean;\n  /** Emphasised rows, e.g. `[0]` for a profit/loss zero line. */\n  highlightRowValues?: number[];\n  /** Default: theme foreground. */\n  highlightRowStroke?: string;\n  /** Default: 0.35 (web P&L examples). */\n  highlightRowStrokeOpacity?: number;\n  highlightRowStrokeWidth?: number;\n  /** Default: [4, 4]. `null` for solid. */\n  highlightRowStrokeDasharray?: [number, number] | null;\n}\n\nexport function Grid({\n  horizontal = true,\n  vertical = false,\n  numTicksRows = 5,\n  numTicksColumns = 10,\n  stroke,\n  strokeWidth = 1,\n  strokeDasharray = [4, 4],\n  fadeHorizontal = true,\n  highlightRowValues,\n  highlightRowStroke,\n  highlightRowStrokeOpacity = 0.35,\n  highlightRowStrokeWidth = 1,\n  highlightRowStrokeDasharray = [4, 4],\n}: GridProps) {\n  const { width, height, margin, xScale, yScale, theme } = useChart();\n  const color = stroke ?? theme.grid;\n  const left = margin.left;\n  const right = width - margin.right;\n\n  // One path for every row → one draw call.\n  const rows = useMemo(() => {\n    const p = Skia.PathBuilder.Make();\n    if (horizontal) {\n      for (const tick of yScale.ticks(numTicksRows)) {\n        const y = Math.round(yScale(tick)) + 0.5;\n        p.moveTo(left, y);\n        p.lineTo(right, y);\n      }\n    }\n    return p.build();\n  }, [horizontal, yScale, numTicksRows, left, right]);\n\n  const columns = useMemo(() => {\n    const p = Skia.PathBuilder.Make();\n    if (vertical) {\n      for (const tick of xScale.ticks(numTicksColumns)) {\n        const x = Math.round(xScale(tick)) + 0.5;\n        p.moveTo(x, margin.top);\n        p.lineTo(x, height - margin.bottom);\n      }\n    }\n    return p.build();\n  }, [vertical, xScale, numTicksColumns, margin, height]);\n\n  const highlightRows = useMemo(() => {\n    const p = Skia.PathBuilder.Make();\n    for (const value of highlightRowValues ?? []) {\n      const y = Math.round(yScale(value)) + 0.5;\n      p.moveTo(left, y);\n      p.lineTo(right, y);\n    }\n    return p.build();\n  }, [highlightRowValues, yScale, left, right]);\n  const highlightColor = highlightRowStroke ?? theme.foreground;\n\n  const dash = strokeDasharray ? (\n    <DashPathEffect intervals={strokeDasharray} />\n  ) : null;\n\n  return (\n    <>\n      <Path color={color} path={rows} strokeWidth={strokeWidth} style=\"stroke\">\n        {dash}\n        {fadeHorizontal ? (\n          <LinearGradient\n            colors={[\"transparent\", color, color, \"transparent\"]}\n            end={vec(right, 0)}\n            positions={[0, 0.1, 0.9, 1]}\n            start={vec(left, 0)}\n          />\n        ) : null}\n      </Path>\n      {vertical ? (\n        <Path\n          color={color}\n          path={columns}\n          strokeWidth={strokeWidth}\n          style=\"stroke\"\n        >\n          {dash}\n        </Path>\n      ) : null}\n      {highlightRowValues?.length ? (\n        <Path\n          color={highlightColor}\n          opacity={highlightRowStrokeOpacity}\n          path={highlightRows}\n          strokeWidth={highlightRowStrokeWidth}\n          style=\"stroke\"\n        >\n          {highlightRowStrokeDasharray ? (\n            <DashPathEffect intervals={highlightRowStrokeDasharray} />\n          ) : null}\n          {fadeHorizontal ? (\n            <LinearGradient\n              colors={[\n                \"transparent\",\n                highlightColor,\n                highlightColor,\n                \"transparent\",\n              ]}\n              end={vec(right, 0)}\n              positions={[0, 0.1, 0.9, 1]}\n              start={vec(left, 0)}\n            />\n          ) : null}\n        </Path>\n      ) : null}\n    </>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/charts/grid.tsx"
    },
    {
      "path": "../native/src/charts/index.ts",
      "content": "export { Area, type AreaProps } from \"./area\";\nexport { AreaChart, type AreaChartProps } from \"./area-chart\";\nexport { Background, type BackgroundProps } from \"./background\";\nexport { Band, type BandProps } from \"./band\";\nexport {\n  type ChartContextValue,\n  type ChartSeries,\n  type Margin,\n  useChart,\n} from \"./chart-context\";\nexport {\n  ChartTooltip,\n  type ChartTooltipProps,\n  type IndicatorFadeEdges,\n} from \"./chart-tooltip\";\nexport { DateTicker, type DateTickerProps } from \"./date-ticker\";\nexport { Grid, type GridProps } from \"./grid\";\nexport { Line, type LineProps } from \"./line\";\nexport { LineChart, type LineChartProps } from \"./line-chart\";\nexport {\n  type ChartMarker,\n  ChartMarkers,\n  type ChartMarkersProps,\n  DiamondMarkers,\n  type DiamondMarkersProps,\n} from \"./markers\";\nexport { PatternArea, type PatternAreaProps } from \"./pattern-area\";\nexport type { PatternPreset } from \"./patterns\";\nexport { ProfitLossLine, type ProfitLossLineProps } from \"./profit-loss-line\";\nexport {\n  buildLinearProjection,\n  ProjectionLine,\n  type ProjectionLineProps,\n  type ProjectionPoint,\n} from \"./projection-line\";\nexport { ReferenceArea, type ReferenceAreaProps } from \"./reference-area\";\nexport {\n  SegmentBackground,\n  type SegmentBackgroundProps,\n  SegmentChangeBadge,\n  type SegmentChangeBadgeProps,\n  SegmentLineFrom,\n  type SegmentLineProps,\n  SegmentLineTo,\n  type SegmentLineVariant,\n} from \"./segment\";\nexport type { FadeEdges } from \"./series-paint\";\nexport {\n  LineSeriesTerminalMarker,\n  type LineSeriesTerminalMarkerProps,\n} from \"./terminal-marker\";\nexport {\n  type ChartSprings,\n  type ChartTheme,\n  DEFAULT_CHART_SPRINGS,\n  darkChartTheme,\n  lightChartTheme,\n  type SpringConfig,\n} from \"./theme\";\nexport type { TimeSeriesChartProps } from \"./time-series-chart\";\nexport { linearFit, TrendLine, type TrendLineProps } from \"./trend-line\";\nexport { Weekends, type WeekendsProps } from \"./weekends\";\nexport { XAxis, type XAxisProps } from \"./x-axis\";\n",
      "type": "registry:component",
      "target": "components/charts/index.ts"
    },
    {
      "path": "../native/src/charts/line-chart.tsx",
      "content": "import {\n  TimeSeriesChart,\n  type TimeSeriesChartProps,\n} from \"./time-series-chart\";\n\nexport type LineChartProps = TimeSeriesChartProps;\n\n/** Compose with `<Line>`, `<Grid>`, `<XAxis>`, `<ChartTooltip>`, segments. */\nexport function LineChart(props: LineChartProps) {\n  return <TimeSeriesChart {...props} />;\n}\n",
      "type": "registry:component",
      "target": "components/charts/line-chart.tsx"
    },
    {
      "path": "../native/src/charts/line.tsx",
      "content": "import { DashPathEffect, Path } from \"@shopify/react-native-skia\";\nimport { type CurveFactory, curveNatural } from \"d3-shape\";\nimport { useMemo } from \"react\";\nimport { collectValues, useChart, withChartPlugin } from \"./chart-context\";\nimport { DATA_TWEEN_MS } from \"./motion\";\nimport {\n  type FadeEdges,\n  HighlightBand,\n  lineBuilder,\n  SeriesLayer,\n  useDimOpacity,\n  useKeyedMorph,\n  useSeriesPoints,\n} from \"./series-paint\";\n\nexport interface LineProps {\n  dataKey: string;\n  /** Tooltip label. Default: `dataKey`. */\n  label?: string;\n  /** Default: theme primary for the first series, secondary after. */\n  stroke?: string;\n  /** Default: 2.5 (matches web). */\n  strokeWidth?: number;\n  /** Dash pattern, e.g. `[6, 4]` for a forecast. Omit for solid. */\n  strokeDasharray?: number[];\n  /** Default: curveNatural (matches web). */\n  curve?: CurveFactory;\n  /** Fade the line out toward the left/right edges. Default: true. */\n  fadeEdges?: FadeEdges;\n  /** Dim the line and re-stroke a band around the scrubbed point. Default: true. */\n  showHighlight?: boolean;\n  /** Morph duration in ms when `data` changes. Default: 500 (web y-domain tween). */\n  morphDuration?: number;\n}\n\nfunction LineImpl({\n  dataKey,\n  stroke,\n  strokeWidth = 2.5,\n  strokeDasharray,\n  curve = curveNatural,\n  fadeEdges = true,\n  showHighlight = true,\n  morphDuration = DATA_TWEEN_MS,\n}: LineProps) {\n  const { series } = useChart();\n  const color =\n    stroke ?? series.find((s) => s.dataKey === dataKey)?.stroke ?? \"#6366f1\";\n\n  const points = useSeriesPoints(dataKey);\n  const build = useMemo(() => lineBuilder(curve), [curve]);\n  const path = useKeyedMorph(points, build, morphDuration);\n  const opacity = useDimOpacity(showHighlight);\n  const dash = strokeDasharray ? (\n    <DashPathEffect intervals={strokeDasharray} />\n  ) : null;\n\n  return (\n    <SeriesLayer fadeEdges={fadeEdges}>\n      <Path\n        color={color}\n        opacity={opacity}\n        path={path}\n        strokeCap=\"round\"\n        strokeJoin=\"round\"\n        strokeWidth={strokeWidth}\n        style=\"stroke\"\n      >\n        {dash}\n      </Path>\n      {showHighlight ? (\n        <HighlightBand>\n          <Path\n            color={color}\n            path={path}\n            strokeCap=\"round\"\n            strokeJoin=\"round\"\n            strokeWidth={strokeWidth}\n            style=\"stroke\"\n          >\n            {dash}\n          </Path>\n        </HighlightBand>\n      ) : null}\n    </SeriesLayer>\n  );\n}\n\nexport const Line = withChartPlugin(LineImpl, {\n  series: (props, index, theme) => ({\n    dataKey: props.dataKey,\n    label: props.label ?? props.dataKey,\n    stroke:\n      props.stroke ?? (index === 0 ? theme.linePrimary : theme.lineSecondary),\n    strokeWidth: props.strokeWidth ?? 2.5,\n  }),\n  yValues: (props, data) => collectValues(data, [props.dataKey]),\n});\n",
      "type": "registry:component",
      "target": "components/charts/line.tsx"
    },
    {
      "path": "../native/src/charts/markers.tsx",
      "content": "import {\n  Circle,\n  DashPathEffect,\n  Group,\n  Paragraph,\n  Path,\n  Skia,\n  Line as SkLine,\n  type SkParagraph,\n  Text,\n  TextAlign,\n  vec,\n} from \"@shopify/react-native-skia\";\nimport { useEffect, useMemo } from \"react\";\nimport {\n  useAnimatedReaction,\n  useDerivedValue,\n  useSharedValue,\n  withDelay,\n  withSpring,\n  withTiming,\n} from \"react-native-reanimated\";\nimport { getDate, useChart } from \"./chart-context\";\nimport { EASE_OUT, PANEL_ENTRANCE_SPRING } from \"./motion\";\n\nexport interface ChartMarker {\n  date: Date;\n  title: string;\n  description?: string;\n  /** Emoji or short glyph for the badge. */\n  icon?: string;\n  /** Badge fill. Default: theme markerBackground. */\n  color?: string;\n}\n\nconst sameDay = (a: Date, b: Date) =>\n  a.getFullYear() === b.getFullYear() &&\n  a.getMonth() === b.getMonth() &&\n  a.getDate() === b.getDate();\n\n/** Data index on the marker's calendar day, or -1. */\nexport function markerIndex(\n  data: Record<string, unknown>[],\n  xDataKey: string,\n  date: Date\n): number {\n  return data.findIndex((d) => sameDay(getDate(d, xDataKey), date));\n}\n\n/** True while the crosshair sits on `index`. */\nfunction useActive(index: number) {\n  const { activeIndex, tooltipOpacity } = useChart();\n  return useDerivedValue(\n    () => tooltipOpacity.value > 0 && activeIndex.value === index\n  );\n}\n\n// ─── Diamond markers (recipe) ────────────────────────────────────────────\n\nexport interface DiamondMarkersProps {\n  items: ChartMarker[];\n  /** Square side before the 45° turn. Default: 8. */\n  size?: number;\n  /** Centre offset above the plot top. Default: 12. */\n  offset?: number;\n}\n\n/** Diamonds above the plot with dashed guides (diamond-markers recipe). */\nexport function DiamondMarkers({\n  items,\n  size = 8,\n  offset = 12,\n}: DiamondMarkersProps) {\n  const { data, xDataKey, reducedMotion } = useChart();\n  const enter = useSharedValue(reducedMotion ? 1 : 0);\n  useEffect(() => {\n    enter.value = withTiming(1, {\n      duration: reducedMotion ? 0 : 300,\n      easing: EASE_OUT,\n    });\n  }, [enter, reducedMotion]);\n\n  return (\n    <Group opacity={enter}>\n      {items.map((m) => (\n        <Diamond\n          index={markerIndex(data, xDataKey, m.date)}\n          key={`${m.date.getTime()}-${m.title}`}\n          marker={m}\n          offset={offset}\n          size={size}\n        />\n      ))}\n    </Group>\n  );\n}\n\nfunction Diamond({\n  marker,\n  index,\n  size,\n  offset,\n}: {\n  marker: ChartMarker;\n  index: number;\n  size: number;\n  offset: number;\n}) {\n  const { xScale, margin, height, theme } = useChart();\n  const active = useActive(index);\n  const x = xScale(marker.date);\n  const cy = margin.top - offset;\n  const half = (size * Math.SQRT2) / 2;\n  const path = useMemo(() => {\n    const b = Skia.PathBuilder.Make();\n    b.moveTo(x, cy - half);\n    b.lineTo(x + half, cy);\n    b.lineTo(x, cy + half);\n    b.lineTo(x - half, cy);\n    b.close();\n    return b.build();\n  }, [x, cy, half]);\n  const lineOpacity = useDerivedValue(() => (active.value ? 1 : 0.7));\n  const { foregroundMuted, linePrimary, markerBorder } = theme;\n  const fill = useDerivedValue(() =>\n    active.value ? linePrimary : foregroundMuted\n  );\n\n  return (\n    <>\n      <SkLine\n        color={markerBorder}\n        opacity={lineOpacity}\n        p1={vec(x, cy + half + 4)}\n        p2={vec(x, height - margin.bottom)}\n        strokeWidth={1}\n      >\n        <DashPathEffect intervals={[4, 4]} />\n      </SkLine>\n      <Path color={fill} path={path} />\n    </>\n  );\n}\n\n// ─── Event markers (ChartMarkers) ────────────────────────────────────────\n\nexport interface ChartMarkersProps {\n  items: ChartMarker[];\n  /** Badge radius. Default: 14 (web size 28). */\n  radius?: number;\n}\n\ninterface MarkerGroupData {\n  key: number;\n  index: number;\n  date: Date;\n  markers: ChartMarker[];\n}\n\nfunction useIconParagraph(icon: string, color: string): SkParagraph | null {\n  return useMemo(() => {\n    if (!icon) {\n      return null;\n    }\n    const p = Skia.ParagraphBuilder.Make({ textAlign: TextAlign.Center })\n      .pushStyle({ fontSize: 14, color: Skia.Color(color) })\n      .addText(icon)\n      .build();\n    p.layout(28);\n    return p;\n  }, [icon, color]);\n}\n\n/** Emoji badges with dashed guides, grouped by day (web `ChartMarkers`). */\nexport function ChartMarkers({ items, radius = 14 }: ChartMarkersProps) {\n  const { data, xDataKey } = useChart();\n  const groups = useMemo(() => {\n    const byDay = new Map<string, MarkerGroupData>();\n    for (const m of items) {\n      const key = m.date.toDateString();\n      const g = byDay.get(key);\n      if (g) {\n        g.markers.push(m);\n      } else {\n        byDay.set(key, {\n          key: m.date.getTime(),\n          index: markerIndex(data, xDataKey, m.date),\n          date: m.date,\n          markers: [m],\n        });\n      }\n    }\n    return [...byDay.values()].sort(\n      (a, b) => a.date.getTime() - b.date.getTime()\n    );\n  }, [items, data, xDataKey]);\n\n  return (\n    <>\n      {groups.map((g, i) => (\n        <MarkerBadge group={g} key={g.key} order={i} radius={radius} />\n      ))}\n    </>\n  );\n}\n\nfunction MarkerBadge({\n  group,\n  order,\n  radius,\n}: {\n  group: MarkerGroupData;\n  order: number;\n  radius: number;\n}) {\n  const {\n    xScale,\n    margin,\n    height,\n    theme,\n    fonts,\n    animationDuration,\n    reducedMotion,\n  } = useChart();\n  const active = useActive(group.index);\n  const x = xScale(group.date);\n  const cy = margin.top - 8;\n  const first = group.markers[0];\n  const count = group.markers.length;\n  const icon = useIconParagraph(first?.icon ?? \"\", theme.markerForeground);\n\n  // Web entrance: scale 0.85 → 1 + fade, spring 300/25, after the reveal,\n  // staggered 100ms per group.\n  const enter = useSharedValue(reducedMotion ? 1 : 0);\n  useEffect(() => {\n    if (!reducedMotion) {\n      enter.value = withDelay(\n        animationDuration + order * 100,\n        withSpring(1, PANEL_ENTRANCE_SPRING)\n      );\n    }\n  }, [enter, reducedMotion, animationDuration, order]);\n  const badgeTransform = useDerivedValue(() => [\n    { scale: 0.85 + 0.15 * enter.value },\n  ]);\n  const badgeOpacity = useDerivedValue(() => Math.min(1, enter.value));\n\n  // Guide: 0.6 at rest, hidden under the crosshair (200ms ease-out).\n  const guide = useSharedValue(0.6);\n  useAnimatedReaction(\n    () => active.value,\n    (isActive, was) => {\n      if (was !== null && isActive !== was) {\n        guide.value = withTiming(isActive ? 0 : 0.6, {\n          duration: 200,\n          easing: EASE_OUT,\n        });\n      }\n    }\n  );\n\n  return (\n    <>\n      <SkLine\n        color={theme.markerBorder}\n        opacity={guide}\n        p1={vec(x, margin.top + 18)}\n        p2={vec(x, height - margin.bottom + 8)}\n        strokeCap=\"round\"\n        strokeWidth={1}\n      >\n        <DashPathEffect intervals={[4, 4]} />\n      </SkLine>\n      <Group\n        opacity={badgeOpacity}\n        origin={vec(x, cy)}\n        transform={badgeTransform}\n      >\n        <Circle color=\"rgba(0,0,0,0.15)\" cx={x} cy={cy + 2} r={radius} />\n        <Circle\n          color={first?.color ?? theme.markerBackground}\n          cx={x}\n          cy={cy}\n          r={radius}\n        />\n        <Circle\n          color={theme.markerBorder}\n          cx={x}\n          cy={cy}\n          r={radius}\n          strokeWidth={1.5}\n          style=\"stroke\"\n        />\n        {icon ? (\n          <Paragraph\n            paragraph={icon}\n            width={28}\n            x={x - 14}\n            y={cy - icon.getHeight() / 2}\n          />\n        ) : null}\n        {count > 1 ? (\n          <CountBadge\n            color={theme.markerBadgeBackground}\n            count={count}\n            font={fonts.badge}\n            textColor={theme.markerBadgeForeground}\n            x={x + 16}\n            y={cy - 16}\n          />\n        ) : null}\n      </Group>\n    </>\n  );\n}\n\nfunction CountBadge({\n  count,\n  x,\n  y,\n  color,\n  textColor,\n  font,\n}: {\n  count: number;\n  x: number;\n  y: number;\n  color: string;\n  textColor: string;\n  font: ReturnType<typeof useChart>[\"fonts\"][\"badge\"];\n}) {\n  const label = String(count);\n  const w = font.getTextWidth(label);\n  return (\n    <>\n      <Circle color={color} cx={x} cy={y} r={9} />\n      <Text\n        color={textColor}\n        font={font}\n        text={label}\n        x={x - w / 2}\n        y={y + 4}\n      />\n    </>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/charts/markers.tsx"
    },
    {
      "path": "../native/src/charts/motion.ts",
      "content": "import { Easing } from \"react-native-reanimated\";\n\n// Every spring here sets mass: 1 to match Motion. Reanimated 4 defaults to 4.\n\n/**\n * Motion constants copied from the web package so native and web feel the\n * same. Source of each value is noted; keep them in sync.\n */\n\n/** `animation.ts` DEFAULT_ANIMATION_EASING: cubic-bezier(0.85, 0, 0.15, 1). */\nexport const CHART_EASING = Easing.bezier(0.85, 0, 0.15, 1);\n\n/** Motion's \"easeInOut\" keyword. */\nexport const EASE_IN_OUT = Easing.bezier(0.42, 0, 0.58, 1);\n\n/** `chart-phase.ts` DEFAULT_Y_DOMAIN_TWEEN_MS: data-change morph. */\nexport const DATA_TWEEN_MS = 500;\n\n/** `tooltip-box.tsx`: outer panel fade in/out. */\nexport const PANEL_FADE_MS = 100;\n\n/** `tooltip-box.tsx` PANEL_ENTRANCE_SPRING + entrance { scale: 0.85, x: ±20 }. */\nexport const PANEL_ENTRANCE_SPRING = { stiffness: 300, damping: 25, mass: 1 };\nexport const PANEL_ENTRANCE_SCALE = 0.85;\nexport const PANEL_ENTRANCE_X = 20;\n\n/** `tooltip-dot.tsx`: dot 2.5 → 5, halo 5 → 13 (×2.6) at 30%, HALO_SPRING. */\nexport const DOT_SPRING = { stiffness: 420, damping: 28, mass: 1 };\nexport const DOT_SIZE = 5;\nexport const DOT_HALO_SCALE = 2.6;\nexport const DOT_HALO_OPACITY = 0.3;\n\n/** `series-hover-dim.tsx` / `highlight-segment.tsx`: 0.4s easeInOut. */\nexport const HOVER_DIM_MS = 400;\n\n/** `segment.tsx` SegmentBackground fade. */\nexport const SEGMENT_FADE_MS = 150;\n\n/** Motion's \"easeOut\" keyword. */\nexport const EASE_OUT = Easing.bezier(0, 0, 0.58, 1);\n\n/** Background / Weekends / TrendLine enter fade (web: 420ms easeOut). */\nexport const ENTER_FADE_MS = 420;\n",
      "type": "registry:component",
      "target": "components/charts/motion.ts"
    },
    {
      "path": "../native/src/charts/pattern-area.tsx",
      "content": "import { Path } from \"@shopify/react-native-skia\";\nimport { type CurveFactory, curveMonotoneX } from \"d3-shape\";\nimport { useMemo } from \"react\";\nimport { collectValues, useChart, withChartPlugin } from \"./chart-context\";\nimport { usePlotRect } from \"./fade-mask\";\nimport { DATA_TWEEN_MS } from \"./motion\";\nimport { PatternFill, type PatternPreset } from \"./patterns\";\nimport {\n  areaBuilder,\n  type FadeEdges,\n  SeriesLayer,\n  useDimOpacity,\n  useKeyedMorph,\n  useSeriesPoints,\n} from \"./series-paint\";\n\nexport interface PatternAreaProps {\n  dataKey: string;\n  label?: string;\n  /** Default: \"diagonal-up\". */\n  pattern?: PatternPreset;\n  /** Pattern colour. Default: theme primary. */\n  color?: string;\n  scale?: number;\n  strokeWidth?: number;\n  /** Faint solid wash under the pattern (0 = none). Default: 0. */\n  fillOpacity?: number;\n  /** Default: curveMonotoneX (web PatternArea). */\n  curve?: CurveFactory;\n  /** Default: true. */\n  fadeEdges?: FadeEdges;\n}\n\n/** Area filled with a pattern; pair with `<Area fillOpacity={0}>` for the stroke. */\nfunction PatternAreaImpl({\n  dataKey,\n  pattern = \"diagonal-up\",\n  color,\n  scale = 1,\n  strokeWidth = 1,\n  fillOpacity = 0,\n  curve = curveMonotoneX,\n  fadeEdges = true,\n}: PatternAreaProps) {\n  const { yScale, theme } = useChart();\n  const rect = usePlotRect();\n  const tint = color ?? theme.linePrimary;\n  const baseline = yScale.range()[0] ?? rect.y + rect.height;\n  const opacity = useDimOpacity(true);\n\n  const points = useSeriesPoints(dataKey, baseline);\n  const build = useMemo(() => areaBuilder(curve), [curve]);\n  const areaPath = useKeyedMorph(points, build, DATA_TWEEN_MS);\n\n  return (\n    <SeriesLayer fadeEdges={fadeEdges}>\n      {fillOpacity > 0 ? (\n        <Path color={tint} opacity={fillOpacity} path={areaPath} />\n      ) : null}\n      <PatternFill\n        clip={areaPath}\n        color={tint}\n        opacity={opacity}\n        preset={pattern}\n        rect={rect}\n        scale={scale}\n        strokeWidth={strokeWidth}\n      />\n    </SeriesLayer>\n  );\n}\n\nexport const PatternArea = withChartPlugin(PatternAreaImpl, {\n  yValues: (props, data) => collectValues(data, [props.dataKey]),\n});\n",
      "type": "registry:component",
      "target": "components/charts/pattern-area.tsx"
    },
    {
      "path": "../native/src/charts/patterns.tsx",
      "content": "import { Group, Path, Skia, type SkPath } from \"@shopify/react-native-skia\";\nimport { useMemo } from \"react\";\nimport type { SharedValue } from \"react-native-reanimated\";\n\n/**\n * Pattern presets ported from web `pattern-preset.tsx` (Nucleo-based tiles).\n * Each tile is `size` px square; `stroke` art is 1 tile-unit wide, `fill`\n * art is solid. Both scale with `scale`.\n */\ninterface PatternDef {\n  size: number;\n  stroke?: string;\n  fill?: string;\n}\n\nconst PATTERNS = {\n  vertical: { size: 6, stroke: \"M5.5 0V6M-0.5 0V6\" },\n  \"vertical-dense\": { size: 3, stroke: \"M2.5 0V3M-0.5 0V3\" },\n  horizontal: { size: 6, stroke: \"M0 5.5H6M0 -0.5H6\" },\n  \"horizontal-dense\": { size: 3, stroke: \"M0 2.5H3M0 -0.5H3\" },\n  \"diagonal-up\": { size: 6, stroke: \"M-2 1L1 -2M-1 6L6 -1M4 7L7 4\" },\n  \"diagonal-up-sparse\": {\n    size: 12,\n    stroke: \"M-2 1L1 -2M-1 12L12 -1M10 13L13 10\",\n  },\n  \"diagonal-down\": { size: 6, stroke: \"M-1 -1L7 7M-2 4L2 8M4 -2L8 2\" },\n  \"diagonal-down-sparse\": {\n    size: 12,\n    stroke: \"M-1 -1L13 13M-2 10L2 14M10 -2L14 2\",\n  },\n  grid: { size: 6, stroke: \"M5.5 0V6M-0.5 0V6M0 5.5H6M0 -0.5H6\" },\n  \"vertical-dashed\": { size: 6, fill: \"M6 1V4H5V1H6Z\" },\n  \"horizontal-dashed\": { size: 6, fill: \"M4 6L1 6L1 5L4 5L4 6Z\" },\n  \"grid-dashed\": { size: 6, fill: \"M6 1V4H5V1H6ZM4 6L1 6L1 5L4 5L4 6Z\" },\n  dots: { size: 6, fill: \"M6 5H5V6H6V5Z\" },\n  \"dots-sparse\": { size: 12, fill: \"M12 11H11V12H12V11ZM6 5H5V6H6V5Z\" },\n  \"diagonal-dots-up\": {\n    size: 6,\n    fill: \"M5 5h1v1h-1zM3 1h1v1h-1zM1 3h1v1h-1z\",\n  },\n  \"diagonal-dots-down\": {\n    size: 6,\n    fill: \"M5 5h1v1h-1zM3 3h1v1h-1zM1 1h1v1h-1z\",\n  },\n  \"circles-dense\": {\n    size: 6,\n    fill: \"M1 2.5a1.5 1.5 0 1 0 3 0a1.5 1.5 0 1 0-3 0z\",\n  },\n  circles: { size: 12, fill: \"M4 5.5a1.5 1.5 0 1 0 3 0a1.5 1.5 0 1 0-3 0z\" },\n  blocks: { size: 6, fill: \"M1 1h3v3h-3z\" },\n  \"checkerboard-xs\": { size: 6, fill: \"M0 0h3v3h-3zM3 3h3v3h-3z\" },\n} as const satisfies Record<string, PatternDef>;\n\nexport type PatternPreset = keyof typeof PATTERNS;\n\nexport interface PatternRect {\n  x: number;\n  y: number;\n  width: number;\n  height: number;\n}\n\n/**\n * Tile a preset across `rect` into one path (one draw call). Tiles are\n * anchored to the rect origin so the pattern doesn't swim on resize.\n */\nexport function usePatternPath(\n  preset: PatternPreset,\n  scale: number,\n  rect: PatternRect\n): { path: SkPath; stroked: boolean } {\n  const { x, y, width, height } = rect;\n  return useMemo(() => {\n    const def: PatternDef = PATTERNS[preset];\n    const art = Skia.Path.MakeFromSVGString(def.stroke ?? def.fill ?? \"\");\n    const builder = Skia.PathBuilder.Make();\n    const tile = def.size * scale;\n    if (art && tile > 0 && width > 0 && height > 0) {\n      const cols = Math.ceil(width / tile) + 1;\n      const rows = Math.ceil(height / tile) + 1;\n      for (let r = 0; r < rows; r++) {\n        for (let c = 0; c < cols; c++) {\n          const m = Skia.Matrix();\n          m.translate(x + c * tile, y + r * tile);\n          m.scale(scale, scale);\n          builder.addPath(art, m);\n        }\n      }\n    }\n    return { path: builder.build(), stroked: Boolean(def.stroke) };\n  }, [preset, scale, x, y, width, height]);\n}\n\nexport interface PatternFillProps {\n  preset: PatternPreset;\n  rect: PatternRect;\n  color: string;\n  scale?: number;\n  /** Line width for stroke presets, in tile units. Default: 1. */\n  strokeWidth?: number;\n  opacity?: number | SharedValue<number>;\n  /** Clip the pattern to this shape (e.g. an area path). */\n  clip?: SkPath | SharedValue<SkPath>;\n}\n\nexport function PatternFill({\n  preset,\n  rect,\n  color,\n  scale = 1,\n  strokeWidth = 1,\n  opacity = 1,\n  clip,\n}: PatternFillProps) {\n  const { path, stroked } = usePatternPath(preset, scale, rect);\n  return (\n    <Group clip={clip} opacity={opacity}>\n      <Path\n        color={color}\n        path={path}\n        strokeWidth={stroked ? strokeWidth * scale : 0}\n        style={stroked ? \"stroke\" : \"fill\"}\n      />\n    </Group>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/charts/patterns.tsx"
    },
    {
      "path": "../native/src/charts/profit-loss-line.tsx",
      "content": "import { Group, Path, rect } from \"@shopify/react-native-skia\";\nimport { type CurveFactory, curveMonotoneX } from \"d3-shape\";\nimport { useMemo } from \"react\";\nimport { collectValues, useChart, withChartPlugin } from \"./chart-context\";\nimport { DATA_TWEEN_MS } from \"./motion\";\nimport {\n  type FadeEdges,\n  lineBuilder,\n  SeriesLayer,\n  useKeyedMorph,\n  useSeriesPoints,\n} from \"./series-paint\";\n\nexport interface ProfitLossLineProps {\n  dataKey: string;\n  /** Tooltip label. Default: \"Profit/Loss\". */\n  label?: string;\n  strokeWidth?: number;\n  positiveColor?: string;\n  negativeColor?: string;\n  /** Default: curveMonotoneX (smooth, no false zero crossings). */\n  curve?: CurveFactory;\n  /** Default: true. */\n  fadeEdges?: FadeEdges;\n}\n\n/**\n * Sign-coloured line: one continuous curve drawn twice and clipped above /\n * below zero, so it stays smooth through crossings.\n */\nfunction ProfitLossLineImpl({\n  dataKey,\n  strokeWidth = 2.5,\n  positiveColor,\n  negativeColor,\n  curve = curveMonotoneX,\n  fadeEdges = true,\n}: ProfitLossLineProps) {\n  const { yScale, width, height, theme } = useChart();\n\n  const points = useSeriesPoints(dataKey);\n  const build = useMemo(() => lineBuilder(curve), [curve]);\n  const path = useKeyedMorph(points, build, DATA_TWEEN_MS);\n  const zeroY = yScale(0);\n\n  const sides = [\n    {\n      key: \"pos\",\n      color: positiveColor ?? theme.positive,\n      clip: rect(0, 0, width, zeroY),\n    },\n    {\n      key: \"neg\",\n      color: negativeColor ?? theme.negative,\n      clip: rect(0, zeroY, width, Math.max(0, height - zeroY)),\n    },\n  ];\n\n  return (\n    <SeriesLayer fadeEdges={fadeEdges}>\n      {sides.map((side) => (\n        <Group clip={side.clip} key={side.key}>\n          <Path\n            color={side.color}\n            path={path}\n            strokeCap=\"round\"\n            strokeJoin=\"round\"\n            strokeWidth={strokeWidth}\n            style=\"stroke\"\n          />\n        </Group>\n      ))}\n    </SeriesLayer>\n  );\n}\n\nexport const ProfitLossLine = withChartPlugin(ProfitLossLineImpl, {\n  series: (props, _index, theme) => ({\n    dataKey: props.dataKey,\n    label: props.label ?? \"Profit/Loss\",\n    stroke: props.positiveColor ?? theme.positive,\n    strokeWidth: props.strokeWidth ?? 2.5,\n    signColors: {\n      positive: props.positiveColor ?? theme.positive,\n      negative: props.negativeColor ?? theme.negative,\n    },\n  }),\n  yValues: (props, data) => collectValues(data, [props.dataKey]),\n});\n",
      "type": "registry:component",
      "target": "components/charts/profit-loss-line.tsx"
    },
    {
      "path": "../native/src/charts/projection-line.tsx",
      "content": "import {\n  DashPathEffect,\n  LinearGradient,\n  Path,\n  Skia,\n  vec,\n} from \"@shopify/react-native-skia\";\nimport { useMemo } from \"react\";\nimport { useDerivedValue } from \"react-native-reanimated\";\nimport { getDate, useChart, withChartPlugin } from \"./chart-context\";\nimport { linearFit } from \"./trend-line\";\n\nexport interface ProjectionPoint {\n  date: Date;\n  value: number;\n}\n\n/**\n * Two-point linear-regression projection from the last row `horizon` steps\n * ahead (web `buildProjectionPath` with autoMethod \"linearRegression\").\n */\nexport function buildLinearProjection(\n  data: Record<string, unknown>[],\n  dataKey: string,\n  horizon: number,\n  xDataKey = \"date\"\n): ProjectionPoint[] {\n  const last = data.at(-1);\n  const prev = data.at(-2);\n  const fit = linearFit(data, xDataKey, dataKey);\n  if (!(last && prev && fit)) {\n    return [];\n  }\n  const t = getDate(last, xDataKey).getTime();\n  const step = t - getDate(prev, xDataKey).getTime();\n  const value = Number(last[dataKey]);\n  return [\n    { date: new Date(t), value },\n    {\n      date: new Date(t + step * horizon),\n      value: value + fit.slope * step * horizon,\n    },\n  ];\n}\n\nexport interface ProjectionLineProps {\n  data: ProjectionPoint[];\n  /** \"bezier\" eases out of and into horizontal tangents. Default: \"linear\". */\n  curveKind?: \"linear\" | \"bezier\";\n  /** Default: theme chart-3. */\n  stroke?: string;\n  /** \"gradient\" blends `gradientStart` → `gradientEnd` along the line. */\n  strokeStyle?: \"solid\" | \"gradient\";\n  gradientStart?: string;\n  gradientEnd?: string;\n  strokeWidth?: number;\n  /** Default: [6, 4]. `[1, 4]` with round caps reads as dots (recipe). */\n  strokeDasharray?: number[];\n}\n\n/** Dashed forecast from the last point (web `ProjectionLine`). */\nfunction ProjectionLineImpl({\n  data: points,\n  curveKind = \"linear\",\n  stroke,\n  strokeStyle = \"solid\",\n  gradientStart,\n  gradientEnd,\n  strokeWidth = 2,\n  strokeDasharray = [6, 4],\n}: ProjectionLineProps) {\n  const { xScale, yScale, theme, revealProgress } = useChart();\n  const color = stroke ?? theme.chart3;\n\n  const { path, p0, p1 } = useMemo(() => {\n    const b = Skia.PathBuilder.Make();\n    const pts = points.map((p) => vec(xScale(p.date), yScale(p.value)));\n    const a = pts[0];\n    const z = pts.at(-1);\n    if (!(a && z)) {\n      return { path: b.build(), p0: vec(0, 0), p1: vec(0, 0) };\n    }\n    b.moveTo(a.x, a.y);\n    if (curveKind === \"bezier\" && pts.length === 2) {\n      const dx = z.x - a.x;\n      b.cubicTo(a.x + 0.45 * dx, a.y, z.x - 0.45 * dx, z.y, z.x, z.y);\n    } else {\n      for (const p of pts.slice(1)) {\n        b.lineTo(p.x, p.y);\n      }\n    }\n    return { path: b.build(), p0: a, p1: z };\n  }, [points, curveKind, xScale, yScale]);\n\n  // Appears once the series has drawn in (web: shown from the reveal phase).\n  const opacity = useDerivedValue(() => (revealProgress.value >= 1 ? 1 : 0));\n\n  return (\n    <Path\n      color={color}\n      opacity={opacity}\n      path={path}\n      strokeCap=\"round\"\n      strokeWidth={strokeWidth}\n      style=\"stroke\"\n    >\n      <DashPathEffect intervals={strokeDasharray} />\n      {strokeStyle === \"gradient\" ? (\n        <LinearGradient\n          colors={[gradientStart ?? theme.linePrimary, gradientEnd ?? color]}\n          end={p1}\n          start={p0}\n        />\n      ) : null}\n    </Path>\n  );\n}\n\nexport const ProjectionLine = withChartPlugin(ProjectionLineImpl, {\n  yValues: (props) => props.data.map((p) => p.value),\n  xValues: (props) => props.data.map((p) => p.date),\n});\n",
      "type": "registry:component",
      "target": "components/charts/projection-line.tsx"
    },
    {
      "path": "../native/src/charts/reference-area.tsx",
      "content": "import {\n  DashPathEffect,\n  Group,\n  Rect,\n  Line as SkLine,\n  vec,\n} from \"@shopify/react-native-skia\";\nimport { useChart, withChartPlugin } from \"./chart-context\";\nimport { EdgeFade, useEnterOpacity, usePlotRect } from \"./fade-mask\";\nimport { withAlpha } from \"./theme\";\n\nexport interface ReferenceAreaProps {\n  /** Value range (horizontal band). */\n  y1?: number;\n  y2?: number;\n  /** Date range (vertical band). */\n  x1?: Date;\n  x2?: Date;\n  /** Default: foregroundMuted at 12% (web DEFAULT_FILL). */\n  fill?: string;\n  fillOpacity?: number;\n  /** Edge lines. Default: \"dashed\". */\n  strokeStyle?: \"solid\" | \"dashed\" | \"none\";\n  stroke?: string;\n  strokeDasharray?: number[];\n  /** Fade fill and edges left/right. Default: true. */\n  fadeEdges?: boolean;\n  /** % of plot width per side. Default: 10. */\n  fadeEdgesLength?: number;\n}\n\n/** Solid band over a value or date range (web `ReferenceArea`). */\nfunction ReferenceAreaImpl({\n  y1,\n  y2,\n  x1,\n  x2,\n  fill,\n  fillOpacity = 1,\n  strokeStyle = \"dashed\",\n  stroke,\n  strokeDasharray = [4, 4],\n  fadeEdges = true,\n  fadeEdgesLength = 10,\n}: ReferenceAreaProps) {\n  const { xScale, yScale, theme } = useChart();\n  const plot = usePlotRect();\n  const enter = useEnterOpacity();\n\n  const top = y2 === undefined ? plot.y : yScale(Math.max(y1 ?? y2, y2));\n  const bottom =\n    y1 === undefined ? plot.y + plot.height : yScale(Math.min(y1, y2 ?? y1));\n  const left = x1 ? xScale(x1) : plot.x;\n  const right = x2 ? xScale(x2) : plot.x + plot.width;\n  const band = {\n    x: Math.max(plot.x, left),\n    y: Math.max(plot.y, top),\n    width: Math.max(\n      0,\n      Math.min(plot.x + plot.width, right) - Math.max(plot.x, left)\n    ),\n    height: Math.max(\n      0,\n      Math.min(plot.y + plot.height, bottom) - Math.max(plot.y, top)\n    ),\n  };\n  const lineColor = stroke ?? theme.foregroundMuted;\n  const horizontal = x1 === undefined && x2 === undefined;\n  const edges = horizontal\n    ? [\n        [vec(band.x, band.y), vec(band.x + band.width, band.y)],\n        [\n          vec(band.x, band.y + band.height),\n          vec(band.x + band.width, band.y + band.height),\n        ],\n      ]\n    : [\n        [vec(band.x, band.y), vec(band.x, band.y + band.height)],\n        [\n          vec(band.x + band.width, band.y),\n          vec(band.x + band.width, band.y + band.height),\n        ],\n      ];\n  const fade = fadeEdges && horizontal ? fadeEdgesLength : 0;\n\n  return (\n    <Group opacity={enter}>\n      <EdgeFade left={fade} rect={plot} right={fade}>\n        <Rect\n          color={fill ?? withAlpha(theme.foregroundMuted, 0.12)}\n          height={band.height}\n          opacity={fillOpacity}\n          width={band.width}\n          x={band.x}\n          y={band.y}\n        />\n        {strokeStyle === \"none\"\n          ? null\n          : edges.map(([p1, p2], i) => (\n              <SkLine color={lineColor} key={i} p1={p1} p2={p2} strokeWidth={1}>\n                {strokeStyle === \"dashed\" ? (\n                  <DashPathEffect intervals={strokeDasharray} />\n                ) : null}\n              </SkLine>\n            ))}\n      </EdgeFade>\n    </Group>\n  );\n}\n\nexport const ReferenceArea = withChartPlugin(ReferenceAreaImpl, {\n  yValues: (props) =>\n    [props.y1, props.y2].filter((v): v is number => v !== undefined),\n});\n",
      "type": "registry:component",
      "target": "components/charts/reference-area.tsx"
    },
    {
      "path": "../native/src/charts/segment.tsx",
      "content": "import {\n  DashPathEffect,\n  Group,\n  Line,\n  LinearGradient,\n  Rect,\n  RoundedRect,\n  Text,\n  vec,\n} from \"@shopify/react-native-skia\";\nimport { useMemo } from \"react\";\nimport { type SharedValue, useDerivedValue } from \"react-native-reanimated\";\nimport { getNumber, useChart } from \"./chart-context\";\nimport { withAlpha } from \"./theme\";\n\n/**\n * Two-finger segment selection, the native take on the web's press-and-drag\n * `SegmentBackground` / `SegmentLineFrom` / `SegmentLineTo`. Selection stays\n * until the next touch.\n */\n\n/** Hidden below this width, like web (5px). */\nconst MIN_SELECTION_PX = 5;\n\nfunction useSelectionOpacity() {\n  const { selectionOpacity, selectionStartX, selectionEndX } = useChart();\n  return useDerivedValue(() =>\n    Math.abs(selectionEndX.value - selectionStartX.value) > MIN_SELECTION_PX\n      ? selectionOpacity.value\n      : 0\n  );\n}\n\n/** Sign of the change in `trendKey` across the selection (UI thread). */\nfunction useTrendUp(trendKey: string | undefined): SharedValue<boolean> {\n  const { data, selectionStartIndex, selectionEndIndex } = useChart();\n  const values = useMemo(\n    () => (trendKey ? data.map((d) => getNumber(d, trendKey)) : []),\n    [data, trendKey]\n  );\n  return useDerivedValue(() => {\n    const start = values[selectionStartIndex.value] ?? 0;\n    const end = values[selectionEndIndex.value] ?? 0;\n    return end >= start;\n  });\n}\n\nexport interface SegmentBackgroundProps {\n  /** Fixed fill. Overrides the trend tint. */\n  fill?: string;\n  /** Tint green (up) / red (down) by the change in this key. */\n  trendKey?: string;\n  /** Trend tint strength. Default: 0.14 (matches homepage). */\n  trendOpacity?: number;\n}\n\nexport function SegmentBackground({\n  fill,\n  trendKey,\n  trendOpacity = 0.14,\n}: SegmentBackgroundProps) {\n  const { height, margin, theme, selectionStartX, selectionEndX } = useChart();\n  const opacity = useSelectionOpacity();\n  const up = useTrendUp(trendKey);\n  const upColor = withAlpha(theme.positive, trendOpacity);\n  const downColor = withAlpha(theme.negative, trendOpacity);\n  const color = useDerivedValue(() => {\n    if (fill) {\n      return fill;\n    }\n    if (!trendKey) {\n      return theme.segmentBackground;\n    }\n    return up.value ? upColor : downColor;\n  });\n  const x = useDerivedValue(() => selectionStartX.value);\n  const w = useDerivedValue(() => selectionEndX.value - selectionStartX.value);\n\n  return (\n    <Rect\n      color={color}\n      height={height - margin.top - margin.bottom}\n      opacity={opacity}\n      width={w}\n      x={x}\n      y={margin.top}\n    />\n  );\n}\n\nexport type SegmentLineVariant = \"dashed\" | \"solid\" | \"gradient\";\n\nexport interface SegmentLineProps {\n  /** Fixed stroke. Overrides the trend tint. */\n  stroke?: string;\n  /** Tint green (up) / red (down) by the change in this key. */\n  trendKey?: string;\n  strokeWidth?: number;\n  /** Default: \"dashed\" (matches web). */\n  variant?: SegmentLineVariant;\n}\n\nfunction SegmentLine({\n  edge,\n  stroke,\n  trendKey,\n  strokeWidth = 1,\n  variant = \"dashed\",\n}: SegmentLineProps & { edge: \"from\" | \"to\" }) {\n  const { height, margin, theme, selectionStartX, selectionEndX } = useChart();\n  const opacity = useSelectionOpacity();\n  const up = useTrendUp(trendKey);\n  const top = margin.top;\n  const bottom = height - margin.bottom;\n  const x = useDerivedValue(() =>\n    edge === \"from\" ? selectionStartX.value : selectionEndX.value\n  );\n  const p1 = useDerivedValue(() => ({ x: x.value, y: top }));\n  const p2 = useDerivedValue(() => ({ x: x.value, y: bottom }));\n  const { positive, negative, segmentLine } = theme;\n  const color = useDerivedValue(() => {\n    if (stroke) {\n      return stroke;\n    }\n    if (!trendKey) {\n      return segmentLine;\n    }\n    return up.value ? positive : negative;\n  });\n\n  if (variant === \"gradient\") {\n    // Gradient colour can't be animated per-trend cheaply; use the static stroke.\n    const c = stroke ?? segmentLine;\n    return (\n      <Line opacity={opacity} p1={p1} p2={p2} strokeWidth={strokeWidth}>\n        <LinearGradient\n          colors={[withAlpha(c, 0), c, c, withAlpha(c, 0)]}\n          end={vec(0, bottom)}\n          positions={[0, 0.1, 0.9, 1]}\n          start={vec(0, top)}\n        />\n      </Line>\n    );\n  }\n\n  return (\n    <Line\n      color={color}\n      opacity={opacity}\n      p1={p1}\n      p2={p2}\n      strokeWidth={strokeWidth}\n    >\n      {variant === \"dashed\" ? <DashPathEffect intervals={[4, 4]} /> : null}\n    </Line>\n  );\n}\n\nexport function SegmentLineFrom(props: SegmentLineProps) {\n  return <SegmentLine {...props} edge=\"from\" />;\n}\n\nexport function SegmentLineTo(props: SegmentLineProps) {\n  return <SegmentLine {...props} edge=\"to\" />;\n}\n\nexport interface SegmentChangeBadgeProps {\n  /** Series whose % change across the selection is shown. */\n  valueKey: string;\n}\n\nconst BADGE_H = 20;\nconst BADGE_PAD_X = 7;\nconst BADGE_CHARS = \"0123456789.,%+-\";\n\n/** % change across the selection, pinned above it (homepage badge). */\nexport function SegmentChangeBadge({ valueKey }: SegmentChangeBadgeProps) {\n  const {\n    data,\n    width,\n    fonts,\n    theme,\n    selectionStartX,\n    selectionEndX,\n    selectionStartIndex,\n    selectionEndIndex,\n  } = useChart();\n  const opacity = useSelectionOpacity();\n\n  const values = useMemo(\n    () => data.map((d) => getNumber(d, valueKey)),\n    [data, valueKey]\n  );\n  // Per-glyph widths so the worklet can measure labels without Skia calls.\n  const charWidths = useMemo(() => {\n    const widths: Record<string, number> = {};\n    for (const ch of BADGE_CHARS) {\n      widths[ch] = fonts.badge.getTextWidth(ch);\n    }\n    return widths;\n  }, [fonts.badge]);\n\n  const change = useDerivedValue(() => {\n    const start = values[selectionStartIndex.value] ?? 0;\n    const end = values[selectionEndIndex.value] ?? 0;\n    return start === 0 ? 0 : (end - start) / start;\n  });\n\n  const label = useDerivedValue(() => {\n    const pct = change.value * 100;\n    const sign = pct >= 0 ? \"+\" : \"-\";\n    return `${sign}${Math.abs(pct).toFixed(1)}%`;\n  });\n\n  const textWidth = useDerivedValue(() => {\n    let w = 0;\n    for (const ch of label.value) {\n      w += charWidths[ch] ?? 0;\n    }\n    return w;\n  });\n\n  const badgeWidth = useDerivedValue(() => textWidth.value + BADGE_PAD_X * 2);\n  const transform = useDerivedValue(() => {\n    const center = (selectionStartX.value + selectionEndX.value) / 2;\n    const w = badgeWidth.value;\n    const x = Math.min(Math.max(center - w / 2, 0), width - w);\n    return [{ translateX: x }, { translateY: 0 }];\n  });\n\n  const { positive, negative } = theme;\n  const color = useDerivedValue(() =>\n    change.value >= 0 ? positive : negative\n  );\n\n  return (\n    <Group opacity={opacity} transform={transform}>\n      <RoundedRect\n        color={color}\n        height={BADGE_H}\n        r={4}\n        width={badgeWidth}\n        x={0}\n        y={0}\n      />\n      <Text\n        color=\"#ffffff\"\n        font={fonts.badge}\n        text={label}\n        x={BADGE_PAD_X}\n        y={BADGE_H / 2 + 4}\n      />\n    </Group>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/charts/segment.tsx"
    },
    {
      "path": "../native/src/charts/series-morph.ts",
      "content": "/**\n * Keyed series morph geometry (pure, no Skia), so a range change zooms\n * instead of popping.\n *\n * Points are keyed by time. The morph runs over the union of old and new\n * keys:\n * - kept keys move from their old pixel position to their new one;\n * - added keys start where the OLD scales would have put them (off the edge\n *   when zooming out) and slide in;\n * - removed keys slide to where the NEW scales put them (off the edge when\n *   zooming in) instead of vanishing.\n *\n * At rest the series is drawn from the new points only.\n */\n\nexport interface KeyedPoint {\n  /** Time (ms). */\n  key: number;\n  x: number;\n  y: number;\n  /** Second y: area baseline or band upper edge. */\n  y0?: number;\n  /** Raw value(s), so a point can be re-projected with other scales. */\n  v: number;\n  v0?: number;\n}\n\n/** Re-project a point with one set of scales. */\nexport type Projector = (p: KeyedPoint) => KeyedPoint;\n\nexport interface SeriesPoints {\n  points: KeyedPoint[];\n  project: Projector;\n}\n\nexport function unionMorph(\n  prev: SeriesPoints,\n  next: SeriesPoints\n): { from: KeyedPoint[]; to: KeyedPoint[] } {\n  const prevBy = new Map(prev.points.map((p) => [p.key, p]));\n  const nextBy = new Map(next.points.map((p) => [p.key, p]));\n  const keys = [...new Set([...prevBy.keys(), ...nextBy.keys()])].sort(\n    (a, b) => a - b\n  );\n  const from: KeyedPoint[] = [];\n  const to: KeyedPoint[] = [];\n  for (const key of keys) {\n    const old = prevBy.get(key);\n    const now = nextBy.get(key);\n    if (old && now) {\n      from.push(old);\n      to.push(now);\n    } else if (now) {\n      from.push(prev.project(now));\n      to.push(now);\n    } else if (old) {\n      from.push(old);\n      to.push(next.project(old));\n    }\n  }\n  return { from, to };\n}\n",
      "type": "registry:component",
      "target": "components/charts/series-morph.ts"
    },
    {
      "path": "../native/src/charts/series-paint.tsx",
      "content": "import {\n  Group,\n  LinearGradient,\n  Mask,\n  Rect,\n  rect,\n  Skia,\n  type SkPath,\n  vec,\n} from \"@shopify/react-native-skia\";\nimport { type CurveFactory, area as d3Area, line as d3Line } from \"d3-shape\";\nimport { type ReactNode, useEffect, useMemo, useRef, useState } from \"react\";\nimport {\n  type SharedValue,\n  useDerivedValue,\n  useSharedValue,\n  withTiming,\n} from \"react-native-reanimated\";\nimport { getDate, getValue, useChart } from \"./chart-context\";\nimport { CHART_EASING } from \"./motion\";\nimport { type KeyedPoint, type SeriesPoints, unionMorph } from \"./series-morph\";\n\n/** `true` = both sides at 15% (web default), or per-side % of plot width. */\nexport type FadeEdges =\n  | boolean\n  | \"left\"\n  | \"right\"\n  | { left?: number; right?: number };\n\nconst DEFAULT_FADE = 15;\n/** Room past the plot edge for round caps and markers. */\nconst PLOT_CLIP_PAD = 6;\n\nexport function resolveFade(fade: FadeEdges): { left: number; right: number } {\n  if (fade === false) {\n    return { left: 0, right: 0 };\n  }\n  if (fade === \"left\") {\n    return { left: DEFAULT_FADE, right: 0 };\n  }\n  if (fade === \"right\") {\n    return { left: 0, right: DEFAULT_FADE };\n  }\n  if (typeof fade === \"object\") {\n    const clamp = (v = 0) => Math.min(50, Math.max(0, v));\n    return { left: clamp(fade.left), right: clamp(fade.right) };\n  }\n  return { left: DEFAULT_FADE, right: DEFAULT_FADE };\n}\n\nexport function toSkPath(d: string | null): SkPath {\n  return (d ? Skia.Path.MakeFromSVGString(d) : null) ?? Skia.Path.Make();\n}\n\n/**\n * Morph from the previous geometry to `target` on the UI thread whenever it\n * changes (data, scales, size). Falls back to a cut when point counts differ.\n */\nexport function useMorphPath(\n  target: SkPath,\n  duration: number\n): SharedValue<SkPath> {\n  const { reducedMotion } = useChart();\n  const [pair, setPair] = useState({ from: target, to: target });\n  const progress = useSharedValue(1);\n\n  useEffect(() => {\n    setPair((prev) =>\n      prev.to === target ? prev : { from: prev.to, to: target }\n    );\n    if (reducedMotion) {\n      progress.value = 1;\n      return;\n    }\n    progress.value = 0;\n    progress.value = withTiming(1, { duration, easing: CHART_EASING });\n  }, [target, reducedMotion, duration, progress]);\n\n  return useDerivedValue(() => {\n    const { from, to } = pair;\n    if (progress.value >= 1 || from === to || !to.isInterpolatable(from)) {\n      return to;\n    }\n    return to.interpolate(from, progress.value) ?? to;\n  });\n}\n\n/**\n * Morph a series whenever its points change — data, size, or a range switch\n * that adds or removes points. See `unionMorph`: the morph runs over old ∪\n * new keys so nothing pops, then the series rests on the new points.\n *\n * `series` must be memoised (see `useSeriesPoints`); `build` per curve.\n */\nexport function useKeyedMorph(\n  series: SeriesPoints,\n  build: (pts: KeyedPoint[]) => SkPath,\n  duration: number\n): SharedValue<SkPath> {\n  const { reducedMotion } = useChart();\n  const progress = useSharedValue(1);\n  /** Generation whose morph has started on the UI thread. */\n  const started = useSharedValue(0);\n  const prevRef = useRef<SeriesPoints | null>(null);\n  const pairRef = useRef<MorphPair | null>(null);\n\n  // Built during render (refs only, no shared-value writes); the effect\n  // below starts the morph. Until it does, the derived value keeps showing\n  // `from` (the old geometry), so there's no flash.\n  const pair = useMemo<MorphPair>(() => {\n    const prev = prevRef.current;\n    const current = pairRef.current;\n    if (prev === series && current) {\n      return current;\n    }\n    prevRef.current = series;\n    const gen = (current?.gen ?? 0) + 1;\n    const rest = build(series.points);\n    let next: MorphPair = { from: rest, to: rest, rest, gen };\n    if (prev && prev.points.length > 0 && !reducedMotion) {\n      const union = unionMorph(prev, series);\n      const from = build(union.from);\n      const to = build(union.to);\n      if (to.isInterpolatable(from)) {\n        next = { from, to, rest, gen };\n      }\n    }\n    pairRef.current = next;\n    return next;\n  }, [series, build, reducedMotion]);\n\n  useEffect(() => {\n    if (pair.from === pair.to) {\n      started.value = pair.gen;\n      progress.value = 1;\n      return;\n    }\n    progress.value = 0;\n    started.value = pair.gen;\n    progress.value = withTiming(1, { duration, easing: CHART_EASING });\n  }, [pair, duration, progress, started]);\n\n  return useDerivedValue(() => {\n    const { from, to, rest, gen } = pair;\n    if (from === to) {\n      return rest;\n    }\n    if (started.value !== gen) {\n      return from;\n    }\n    if (progress.value >= 1) {\n      return rest;\n    }\n    return to.interpolate(from, progress.value) ?? rest;\n  });\n}\n\ninterface MorphPair {\n  from: SkPath;\n  to: SkPath;\n  /** New points only, drawn at rest. */\n  rest: SkPath;\n  gen: number;\n}\n\n/**\n * Pixel points for `yKey` (rows where it's defined), keyed by time, plus a\n * projector for these scales. `y0` is a second key (band upper edge) or a\n * fixed pixel baseline (area).\n */\nexport function useSeriesPoints(\n  yKey: string,\n  y0?: string | number\n): SeriesPoints {\n  const { data, xDataKey, xScale, yScale } = useChart();\n  return useMemo(() => {\n    const project = (p: KeyedPoint): KeyedPoint => ({\n      ...p,\n      x: xScale(new Date(p.key)),\n      y: yScale(p.v),\n      y0: baselineY(y0, p.v0, yScale),\n    });\n    const points: KeyedPoint[] = [];\n    for (const d of data) {\n      const v = getValue(d, yKey);\n      const v0 = typeof y0 === \"string\" ? getValue(d, y0) : null;\n      if (v === null || (typeof y0 === \"string\" && v0 === null)) {\n        continue;\n      }\n      points.push(\n        project({\n          key: getDate(d, xDataKey).getTime(),\n          x: 0,\n          y: 0,\n          v,\n          v0: v0 ?? undefined,\n        })\n      );\n    }\n    return { points, project };\n  }, [data, xDataKey, xScale, yScale, yKey, y0]);\n}\n\nfunction baselineY(\n  y0: string | number | undefined,\n  v0: number | undefined,\n  yScale: (v: number) => number\n): number | undefined {\n  if (typeof y0 === \"number\") {\n    return y0;\n  }\n  return v0 === undefined ? undefined : yScale(v0);\n}\n\n/** Line through `y` (or `y0`) of keyed points. Memoise per curve. */\nexport function lineBuilder(curve: CurveFactory, field: \"y\" | \"y0\" = \"y\") {\n  return (pts: KeyedPoint[]) =>\n    toSkPath(\n      d3Line<KeyedPoint>()\n        .x((p) => p.x)\n        .y((p) => (field === \"y\" ? p.y : (p.y0 ?? p.y)))\n        .curve(curve)(pts)\n    );\n}\n\n/** Area between `y` and `y0` of keyed points. Memoise per curve. */\nexport function areaBuilder(curve: CurveFactory) {\n  return (pts: KeyedPoint[]) =>\n    toSkPath(\n      d3Area<KeyedPoint>()\n        .x((p) => p.x)\n        .y0((p) => p.y0 ?? p.y)\n        .y1((p) => p.y)\n        .curve(curve)(pts)\n    );\n}\n\n/**\n * Wraps a series: left-to-right reveal clip, then the horizontal edge fade\n * as an alpha mask so fill and stroke fade together.\n */\nexport function SeriesLayer({\n  fadeEdges,\n  children,\n}: {\n  fadeEdges: FadeEdges;\n  children: ReactNode;\n}) {\n  const { width, height, margin, revealProgress } = useChart();\n  const { left, right } = resolveFade(fadeEdges);\n  const plotLeft = margin.left;\n  const plotRight = width - margin.right;\n\n  // Left-to-right reveal, and keep the series inside the plot so points\n  // sliding off during a range morph never draw in the margins.\n  const reveal = useDerivedValue(() => {\n    const left = plotLeft - PLOT_CLIP_PAD;\n    const right = Math.min(\n      plotLeft + revealProgress.value * (width - plotLeft),\n      plotRight + PLOT_CLIP_PAD\n    );\n    return rect(left, 0, Math.max(0, right - left), height);\n  });\n\n  const content = <Group clip={reveal}>{children}</Group>;\n  if (left === 0 && right === 0) {\n    return content;\n  }\n\n  const l = left / 100;\n  const r = 1 - right / 100;\n  return (\n    <Mask\n      mask={\n        <Rect height={height} width={width} x={0} y={0}>\n          <LinearGradient\n            colors={[\n              left > 0 ? \"rgba(0,0,0,0)\" : \"#000\",\n              \"#000\",\n              \"#000\",\n              right > 0 ? \"rgba(0,0,0,0)\" : \"#000\",\n            ]}\n            end={vec(plotRight, 0)}\n            positions={[0, l, r, 1]}\n            start={vec(plotLeft, 0)}\n          />\n        </Rect>\n      }\n    >\n      {content}\n    </Mask>\n  );\n}\n\n/** Web default: the rest of the series drops to 30% while scrubbing. */\nexport const HIGHLIGHT_DIM_OPACITY = 0.3;\n\n/** Series opacity: dims while the tooltip is up (when highlight is on). */\nexport function useDimOpacity(enabled: boolean) {\n  const { highlightOpacity } = useChart();\n  return useDerivedValue(() =>\n    enabled ? 1 - highlightOpacity.value * (1 - HIGHLIGHT_DIM_OPACITY) : 1\n  );\n}\n\n/**\n * Re-draws `children` at full strength inside the springing hover band\n * (±1 point around the active index).\n */\nexport function HighlightBand({ children }: { children: ReactNode }) {\n  const { height, highlightX, highlightWidth, highlightOpacity } = useChart();\n  const band = useDerivedValue(() =>\n    rect(highlightX.value, 0, Math.max(0, highlightWidth.value), height)\n  );\n  return (\n    <Group clip={band} opacity={highlightOpacity}>\n      {children}\n    </Group>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/charts/series-paint.tsx"
    },
    {
      "path": "../native/src/charts/terminal-marker.tsx",
      "content": "import { Circle, Group } from \"@shopify/react-native-skia\";\nimport { useEffect, useMemo } from \"react\";\nimport {\n  Easing,\n  useDerivedValue,\n  useSharedValue,\n  withDelay,\n  withTiming,\n} from \"react-native-reanimated\";\nimport { getDate, getValue, useChart } from \"./chart-context\";\n\nexport interface LineSeriesTerminalMarkerProps {\n  dataKey: string;\n  /** Default: theme primary. */\n  stroke?: string;\n  /** Dot fill. Default: `stroke`. `\"transparent\"` for ring only. */\n  fill?: string;\n  /** Gap between point and ring. Default: 6 (recipe). */\n  ringGap?: number;\n  size?: number;\n}\n\nconst ENTER_EASING = Easing.bezier(0.22, 1, 0.36, 1);\n\n/** Dot and ring at the series' last point (web `LineSeriesTerminalMarker`). */\nexport function LineSeriesTerminalMarker({\n  dataKey,\n  stroke,\n  fill,\n  ringGap = 6,\n  size = 5,\n}: LineSeriesTerminalMarkerProps) {\n  const {\n    data,\n    xDataKey,\n    xScale,\n    yScale,\n    theme,\n    animationDuration,\n    reducedMotion,\n  } = useChart();\n  const color = stroke ?? theme.linePrimary;\n  const point = useMemo(() => {\n    for (let i = data.length - 1; i >= 0; i--) {\n      const d = data[i] ?? {};\n      const v = getValue(d, dataKey);\n      if (v !== null) {\n        return { x: xScale(getDate(d, xDataKey)), y: yScale(v) };\n      }\n    }\n    return null;\n  }, [data, dataKey, xDataKey, xScale, yScale]);\n\n  // Fades and scales in (0.55 → 1, 280ms) once the line has drawn.\n  const progress = useSharedValue(reducedMotion ? 1 : 0);\n  useEffect(() => {\n    if (!reducedMotion) {\n      progress.value = withDelay(\n        animationDuration,\n        withTiming(1, { duration: 280, easing: ENTER_EASING })\n      );\n    }\n  }, [progress, reducedMotion, animationDuration]);\n  const transform = useDerivedValue(() => [\n    { scale: 0.55 + 0.45 * progress.value },\n  ]);\n\n  if (!point) {\n    return null;\n  }\n  return (\n    <Group\n      opacity={progress}\n      origin={{ x: point.x, y: point.y }}\n      transform={transform}\n    >\n      <Circle color={fill ?? color} cx={point.x} cy={point.y} r={size} />\n      <Circle\n        color={color}\n        cx={point.x}\n        cy={point.y}\n        r={size + ringGap + 0.75}\n        strokeWidth={1.5}\n        style=\"stroke\"\n      />\n    </Group>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/charts/terminal-marker.tsx"
    },
    {
      "path": "../native/src/charts/theme.ts",
      "content": "/**\n * Native counterpart of the `--chart-*` CSS variables in\n * `packages/studio/src/styles/bklit-tokens.css`. Skia can't read CSS\n * variables or parse `oklch()`, so the tokens are resolved to hex here.\n */\nexport interface ChartTheme {\n  linePrimary: string;\n  lineSecondary: string;\n  grid: string;\n  crosshair: string;\n  label: string;\n  tooltipBackground: string;\n  tooltipForeground: string;\n  tooltipMuted: string;\n  tooltipBorder: string;\n  /** Ring drawn around tooltip dots so they read against the line. */\n  dotRing: string;\n  /** Date ticker pill (web: zinc-900 / zinc-100). */\n  tickerBackground: string;\n  tickerForeground: string;\n  segmentBackground: string;\n  segmentLine: string;\n  positive: string;\n  negative: string;\n  foreground: string;\n  /** `--chart-foreground-muted`. */\n  foregroundMuted: string;\n  /** `--chart-3` (purple). */\n  chart3: string;\n  /** `--chart-5` (black / white). */\n  chart5: string;\n  markerBackground: string;\n  markerBorder: string;\n  markerForeground: string;\n  markerBadgeBackground: string;\n  markerBadgeForeground: string;\n}\n\nexport const lightChartTheme: ChartTheme = {\n  linePrimary: \"#6366f1\",\n  lineSecondary: \"#facc15\",\n  grid: \"#e4e4e4\",\n  crosshair: \"#9a9a9a\",\n  label: \"#5c5f67\",\n  tooltipBackground: \"#ffffff\",\n  tooltipForeground: \"#0a0a0a\",\n  tooltipMuted: \"#737373\",\n  tooltipBorder: \"#e5e5e5\",\n  dotRing: \"#ffffff\",\n  tickerBackground: \"#18181b\",\n  tickerForeground: \"#ffffff\",\n  segmentBackground: \"rgba(176, 176, 176, 0.12)\",\n  segmentLine: \"rgba(80, 80, 80, 0.35)\",\n  positive: \"#10b981\",\n  negative: \"#ef4444\",\n  foreground: \"#0a0a0a\",\n  foregroundMuted: \"#d1d1d3\",\n  chart3: \"#6a3b9e\",\n  chart5: \"#000000\",\n  markerBackground: \"#c8c8ca\",\n  markerBorder: \"#cdcdd0\",\n  markerForeground: \"#2e2e31\",\n  markerBadgeBackground: \"#000000\",\n  markerBadgeForeground: \"#cacaca\",\n};\n\nexport const darkChartTheme: ChartTheme = {\n  linePrimary: \"#6366f1\",\n  lineSecondary: \"#facc15\",\n  grid: \"rgba(130, 137, 148, 0.25)\",\n  crosshair: \"#a8a9b8\",\n  label: \"#b5b8c0\",\n  tooltipBackground: \"#171717\",\n  tooltipForeground: \"#fafafa\",\n  tooltipMuted: \"#a3a3a3\",\n  tooltipBorder: \"#262626\",\n  dotRing: \"#0a0a0a\",\n  tickerBackground: \"#f4f4f5\",\n  tickerForeground: \"#18181b\",\n  segmentBackground: \"rgba(70, 80, 92, 0.3)\",\n  segmentLine: \"rgba(255, 255, 255, 0.25)\",\n  positive: \"#10b981\",\n  negative: \"#ef4444\",\n  foreground: \"#fafafa\",\n  foregroundMuted: \"#8e8f94\",\n  chart3: \"#6a3b9e\",\n  chart5: \"#ffffff\",\n  markerBackground: \"#222225\",\n  markerBorder: \"#48484c\",\n  markerForeground: \"#dedede\",\n  markerBadgeBackground: \"#ffffff\",\n  markerBadgeForeground: \"#0b0b0b\",\n};\n\n/**\n * Same shape and defaults as web `ChartConfig` springs. Mass is pinned to 1\n * (Motion's default); Reanimated 4 defaults to mass 4, which halves the\n * damping ratio and doubles the bounce.\n */\nexport interface SpringConfig {\n  stiffness: number;\n  damping: number;\n  mass?: number;\n}\n\n/** Resolve to a Reanimated spring config with Motion's mass of 1. */\nexport function toSpring(config: SpringConfig) {\n  return { mass: 1, ...config };\n}\n\nexport interface ChartSprings {\n  /** Crosshair, dots, date ticker position. */\n  tooltip: SpringConfig;\n  /** Floating tooltip panel; softer so it trails the crosshair. */\n  tooltipBox: SpringConfig;\n  /** Hover-highlight band (x + width). */\n  highlight: SpringConfig;\n  /** Date ticker roll + width. */\n  ticker: SpringConfig;\n}\n\nexport const DEFAULT_CHART_SPRINGS: ChartSprings = {\n  tooltip: { stiffness: 300, damping: 30 },\n  tooltipBox: { stiffness: 100, damping: 20 },\n  highlight: { stiffness: 180, damping: 28 },\n  ticker: { stiffness: 400, damping: 35 },\n};\n\n/** Set the alpha of a #rrggbb / #rrggbbaa colour (others pass through). */\nexport function withAlpha(hex: string, alpha: number): string {\n  if (!hex.startsWith(\"#\") || (hex.length !== 7 && hex.length !== 9)) {\n    return hex;\n  }\n  const a = Math.round(Math.min(1, Math.max(0, alpha)) * 255)\n    .toString(16)\n    .padStart(2, \"0\");\n  return `${hex.slice(0, 7)}${a}`;\n}\n",
      "type": "registry:component",
      "target": "components/charts/theme.ts"
    },
    {
      "path": "../native/src/charts/time-series-chart.tsx",
      "content": "import { Canvas, matchFont } from \"@shopify/react-native-skia\";\nimport { extent, max, min } from \"d3-array\";\nimport { scaleLinear, scaleTime } from \"d3-scale\";\nimport {\n  Children,\n  isValidElement,\n  type ReactNode,\n  useCallback,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport {\n  type LayoutChangeEvent,\n  Platform,\n  type StyleProp,\n  useColorScheme,\n  View,\n  type ViewStyle,\n} from \"react-native\";\nimport {\n  GestureDetector,\n  type GestureTouchEvent,\n  useCompetingGestures,\n  usePanGesture,\n} from \"react-native-gesture-handler\";\nimport {\n  useReducedMotion,\n  useSharedValue,\n  withSpring,\n  withTiming,\n} from \"react-native-reanimated\";\nimport { scheduleOnRN } from \"react-native-worklets\";\nimport {\n  ChartContext,\n  type ChartContextValue,\n  type ChartFonts,\n  type ChartSeries,\n  getChartPlugin,\n  getDate,\n  type Margin,\n  nearestIndex,\n} from \"./chart-context\";\nimport {\n  CHART_EASING,\n  DOT_SPRING,\n  EASE_IN_OUT,\n  HOVER_DIM_MS,\n  PANEL_ENTRANCE_SPRING,\n  PANEL_FADE_MS,\n  SEGMENT_FADE_MS,\n} from \"./motion\";\nimport {\n  type ChartSprings,\n  type ChartTheme,\n  DEFAULT_CHART_SPRINGS,\n  darkChartTheme,\n  lightChartTheme,\n  toSpring,\n} from \"./theme\";\n\nexport interface TimeSeriesChartProps {\n  data: Record<string, unknown>[];\n  xDataKey?: string;\n  margin?: Partial<Margin>;\n  /** Width ÷ height. Ignored when `height` is set. Default: 2 (web \"2 / 1\"). */\n  aspectRatio?: number;\n  height?: number;\n  /** Reveal duration in ms. Default: 1100 (matches web). */\n  animationDuration?: number;\n  /** Override resolved light/dark tokens. */\n  theme?: Partial<ChartTheme>;\n  /** Override tooltip / panel / highlight / ticker springs. */\n  springs?: Partial<ChartSprings>;\n  /** JS-thread callback when the scrubbed index changes; `null` on release. */\n  onActiveIndexChange?: (index: number | null) => void;\n  /** Override the x domain (e.g. to leave room for a forecast). */\n  xDomain?: [Date, Date];\n  /** JS-thread callback when a two-finger selection settles on new indices. */\n  onSelectionChange?: (range: { start: number; end: number } | null) => void;\n  style?: StyleProp<ViewStyle>;\n  children: ReactNode;\n}\n\n// Bottom 40 (web) leaves room for the 32px date pill centred under the plot.\nconst DEFAULT_MARGIN: Margin = { top: 16, right: 16, bottom: 40, left: 16 };\n/** Gap between crosshair and floating panel (web tooltip-box offset). */\nexport const TOOLTIP_OFFSET = 16;\n\nconst fontFamily = Platform.select({\n  ios: \"Helvetica Neue\",\n  default: \"sans-serif\",\n});\n\ninterface ChartLayoutInputs {\n  series: ChartSeries[];\n  yValues: number[];\n  xValues: Date[];\n}\n\n/** Walk children (and fragments/groups) asking each plugin for its inputs. */\nfunction collectInputs(\n  children: ReactNode,\n  data: Record<string, unknown>[],\n  theme: ChartTheme\n): ChartLayoutInputs {\n  const out: ChartLayoutInputs = { series: [], yValues: [], xValues: [] };\n  const visit = (node: ReactNode) => {\n    Children.forEach(node, (child) => {\n      if (!isValidElement(child)) {\n        return;\n      }\n      const plugin = getChartPlugin(child.type);\n      const props = child.props as Record<string, unknown>;\n      if (plugin) {\n        const s = plugin.series?.(props, out.series.length, theme);\n        if (s) {\n          out.series.push(s);\n        }\n        out.yValues.push(...(plugin.yValues?.(props, data) ?? []));\n        out.xValues.push(...(plugin.xValues?.(props, data) ?? []));\n        return;\n      }\n      const nested = props.children as ReactNode;\n      if (nested) {\n        visit(nested);\n      }\n    });\n  };\n  visit(children);\n  return out;\n}\n\nfunction useScales({\n  data,\n  xDataKey,\n  inputs,\n  xDomain,\n  margin,\n  width,\n  height,\n}: {\n  data: Record<string, unknown>[];\n  xDataKey: string;\n  inputs: ChartLayoutInputs;\n  xDomain?: [Date, Date];\n  margin: Margin;\n  width: number;\n  height: number;\n}) {\n  const xScale = useMemo(() => {\n    const dates = [...data.map((d) => getDate(d, xDataKey)), ...inputs.xValues];\n    const [min = new Date(), maxDate = new Date()] = xDomain ?? extent(dates);\n    return scaleTime()\n      .domain([min, maxDate])\n      .range([margin.left, Math.max(margin.left, width - margin.right)]);\n  }, [data, xDataKey, inputs, xDomain, margin, width]);\n\n  const yScale = useMemo(() => {\n    const values = inputs.yValues;\n    const lo = Math.min(0, min(values) ?? 0);\n    const hi = Math.max(0, max(values) ?? 1);\n    return scaleLinear()\n      .domain([lo, hi === lo ? lo + 1 : hi])\n      .nice()\n      .range([Math.max(margin.top, height - margin.bottom), margin.top]);\n  }, [inputs, margin, height]);\n\n  const xs = useMemo(\n    () => data.map((d) => xScale(getDate(d, xDataKey))),\n    [data, xScale, xDataKey]\n  );\n\n  return { xScale, yScale, xs };\n}\n\nexport function TimeSeriesChart({\n  data,\n  xDataKey = \"date\",\n  margin: marginProp,\n  aspectRatio = 2,\n  height: heightProp,\n  animationDuration = 1100,\n  theme: themeOverride,\n  springs: springsOverride,\n  xDomain,\n  onActiveIndexChange,\n  onSelectionChange,\n  style,\n  children,\n}: TimeSeriesChartProps) {\n  const scheme = useColorScheme();\n  const reducedMotion = useReducedMotion();\n  const [width, setWidth] = useState(0);\n  const height = heightProp ?? (width > 0 ? width / aspectRatio : 0);\n\n  const theme = useMemo(\n    () => ({\n      ...(scheme === \"dark\" ? darkChartTheme : lightChartTheme),\n      ...themeOverride,\n    }),\n    [scheme, themeOverride]\n  );\n  const springs = useMemo(() => {\n    const merged = { ...DEFAULT_CHART_SPRINGS, ...springsOverride };\n    return {\n      tooltip: toSpring(merged.tooltip),\n      tooltipBox: toSpring(merged.tooltipBox),\n      highlight: toSpring(merged.highlight),\n      ticker: toSpring(merged.ticker),\n    };\n  }, [springsOverride]);\n  const margin = useMemo(\n    () => ({ ...DEFAULT_MARGIN, ...marginProp }),\n    [marginProp]\n  );\n\n  const fonts = useMemo<ChartFonts>(\n    () => ({\n      axis: matchFont({ fontFamily, fontSize: 11, fontWeight: \"500\" }),\n      tooltip: matchFont({ fontFamily, fontSize: 13, fontWeight: \"400\" }),\n      tooltipStrong: matchFont({ fontFamily, fontSize: 13, fontWeight: \"600\" }),\n      ticker: matchFont({ fontFamily, fontSize: 14, fontWeight: \"500\" }),\n      badge: matchFont({ fontFamily, fontSize: 11, fontWeight: \"700\" }),\n    }),\n    []\n  );\n\n  const inputs = useMemo(\n    () => collectInputs(children, data, theme),\n    [children, data, theme]\n  );\n  const series = inputs.series;\n\n  const { xScale, yScale, xs } = useScales({\n    data,\n    xDataKey,\n    inputs,\n    xDomain,\n    margin,\n    width,\n    height,\n  });\n\n  const revealProgress = useSharedValue(reducedMotion ? 1 : 0);\n  const activeIndex = useSharedValue(0);\n  const crosshairX = useSharedValue(0);\n  const tooltipOpacity = useSharedValue(0);\n  const boxX = useSharedValue(0);\n  const boxWidth = useSharedValue(0);\n  const tickerWidth = useSharedValue(0);\n  const highlightX = useSharedValue(0);\n  const highlightWidth = useSharedValue(0);\n  const selectionStartX = useSharedValue(0);\n  const selectionEndX = useSharedValue(0);\n  const selectionStartIndex = useSharedValue(0);\n  const selectionEndIndex = useSharedValue(0);\n  const selectionOpacity = useSharedValue(0);\n  const panelOpacity = useSharedValue(0);\n  const panelEntrance = useSharedValue(1);\n  const panelFlipped = useSharedValue(0);\n  const dotEntrance = useSharedValue(1);\n  const highlightOpacity = useSharedValue(0);\n\n  // Worklets read layout through shared values so the gesture config can stay\n  // stable across renders.\n  const xsShared = useSharedValue(xs);\n  const plotRight = useSharedValue(width - margin.right);\n  const plotLeft = useSharedValue(margin.left);\n  useEffect(() => {\n    xsShared.value = xs;\n    plotRight.value = width - margin.right;\n    plotLeft.value = margin.left;\n  }, [xs, width, margin, xsShared, plotRight, plotLeft]);\n\n  const hasRevealed = useRef(false);\n  useEffect(() => {\n    if (width === 0 || hasRevealed.current) {\n      return;\n    }\n    hasRevealed.current = true;\n    revealProgress.value = reducedMotion\n      ? 1\n      : withTiming(1, { duration: animationDuration, easing: CHART_EASING });\n  }, [width, reducedMotion, animationDuration, revealProgress]);\n\n  const onIndexRef = useRef(onActiveIndexChange);\n  onIndexRef.current = onActiveIndexChange;\n  const notifyIndex = useCallback((index: number | null) => {\n    onIndexRef.current?.(index);\n  }, []);\n  const onSelectionRef = useRef(onSelectionChange);\n  onSelectionRef.current = onSelectionChange;\n  const notifySelection = useCallback((start: number, end: number) => {\n    onSelectionRef.current?.(start < 0 ? null : { start, end });\n  }, []);\n\n  const instant = reducedMotion;\n  const tooltipSpring = springs.tooltip;\n  const boxSpring = springs.tooltipBox;\n  const highlightSpring = springs.highlight;\n\n  const moveTo = (x: number, jump: boolean) => {\n    \"worklet\";\n    const arr = xsShared.value;\n    const n = arr.length;\n    if (n === 0) {\n      return;\n    }\n    const i = nearestIndex(arr, x);\n    const changed = i !== activeIndex.value;\n    if (!(jump || changed)) {\n      return;\n    }\n    activeIndex.value = i;\n    const cx = arr[i] ?? 0;\n    const w = boxWidth.value;\n    const flip = cx + TOOLTIP_OFFSET + w > plotRight.value;\n    const bx = flip ? cx - TOOLTIP_OFFSET - w : cx + TOOLTIP_OFFSET;\n    const hx = arr[Math.max(0, i - 1)] ?? cx;\n    const hw = (arr[Math.min(n - 1, i + 1)] ?? cx) - hx;\n    if (!(jump || instant) && flip !== (panelFlipped.value === 1)) {\n      // Web remounts the panel on flip, replaying its entrance.\n      panelEntrance.value = 0;\n      panelEntrance.value = withSpring(1, PANEL_ENTRANCE_SPRING);\n    }\n    panelFlipped.value = flip ? 1 : 0;\n    if (jump || instant) {\n      crosshairX.value = cx;\n      boxX.value = bx;\n      highlightX.value = hx;\n      highlightWidth.value = hw;\n    } else {\n      crosshairX.value = withSpring(cx, tooltipSpring);\n      boxX.value = withSpring(bx, boxSpring);\n      highlightX.value = withSpring(hx, highlightSpring);\n      highlightWidth.value = withSpring(hw, highlightSpring);\n    }\n    scheduleOnRN(notifyIndex, i);\n  };\n\n  const ms = (duration: number) => {\n    \"worklet\";\n    return instant ? 0 : duration;\n  };\n\n  // Web: crosshair, dots and ticker mount/unmount instantly; the panel fades\n  // (100ms) and springs in; the series dim eases over 400ms.\n  const show = () => {\n    \"worklet\";\n    tooltipOpacity.value = 1;\n    panelOpacity.value = withTiming(1, { duration: ms(PANEL_FADE_MS) });\n    highlightOpacity.value = withTiming(1, {\n      duration: ms(HOVER_DIM_MS),\n      easing: EASE_IN_OUT,\n    });\n    if (!instant) {\n      panelEntrance.value = 0;\n      panelEntrance.value = withSpring(1, PANEL_ENTRANCE_SPRING);\n      dotEntrance.value = 0;\n      dotEntrance.value = withSpring(1, DOT_SPRING);\n    }\n  };\n\n  const hide = () => {\n    \"worklet\";\n    tooltipOpacity.value = 0;\n    panelOpacity.value = withTiming(0, { duration: ms(PANEL_FADE_MS) });\n    highlightOpacity.value = withTiming(0, {\n      duration: ms(HOVER_DIM_MS),\n      easing: EASE_IN_OUT,\n    });\n  };\n\n  const scrub = usePanGesture({\n    // One finger scrubs horizontally; vertical drags fall through to scroll.\n    maxPointers: 1,\n    activeOffsetX: [-6, 6],\n    failOffsetY: [-12, 12],\n    onBegin: (e) => {\n      \"worklet\";\n      selectionOpacity.value = withTiming(0, { duration: ms(SEGMENT_FADE_MS) });\n      const hidden = tooltipOpacity.value === 0;\n      moveTo(e.x, hidden);\n      if (hidden) {\n        show();\n      }\n    },\n    onUpdate: (e) => {\n      \"worklet\";\n      moveTo(e.x, false);\n    },\n    onFinalize: () => {\n      \"worklet\";\n      hide();\n      scheduleOnRN(notifyIndex, null);\n    },\n  });\n\n  const updateSelection = (e: GestureTouchEvent) => {\n    \"worklet\";\n    const a = e.allTouches[0];\n    const b = e.allTouches[1];\n    const arr = xsShared.value;\n    if (!(a && b) || arr.length < 2) {\n      return;\n    }\n    const lo = Math.max(plotLeft.value, Math.min(a.x, b.x));\n    const hi = Math.min(plotRight.value, Math.max(a.x, b.x));\n    const si = nearestIndex(arr, lo);\n    const ei = nearestIndex(arr, hi);\n    selectionStartX.value = lo;\n    selectionEndX.value = hi;\n    if (si !== selectionStartIndex.value || ei !== selectionEndIndex.value) {\n      selectionStartIndex.value = si;\n      selectionEndIndex.value = ei;\n      scheduleOnRN(notifySelection, si, ei);\n    }\n    if (tooltipOpacity.value !== 0) {\n      hide();\n    }\n    if (selectionOpacity.value < 1) {\n      selectionOpacity.value = withTiming(1, {\n        duration: ms(SEGMENT_FADE_MS),\n      });\n    }\n  };\n\n  // Two fingers select a segment; it stays until the next touch.\n  const select = usePanGesture({\n    minPointers: 2,\n    onTouchesDown: (e) => {\n      \"worklet\";\n      updateSelection(e);\n    },\n    onTouchesMove: (e) => {\n      \"worklet\";\n      updateSelection(e);\n    },\n  });\n\n  const gesture = useCompetingGestures(scrub, select);\n\n  const onLayout = useCallback((e: LayoutChangeEvent) => {\n    setWidth(Math.round(e.nativeEvent.layout.width));\n  }, []);\n\n  const ctx: ChartContextValue = {\n    data,\n    xDataKey,\n    width,\n    height,\n    margin,\n    xScale,\n    yScale,\n    series,\n    xs,\n    theme,\n    fonts,\n    springs,\n    animationDuration,\n    reducedMotion,\n    revealProgress,\n    activeIndex,\n    crosshairX,\n    tooltipOpacity,\n    boxX,\n    boxWidth,\n    tickerWidth,\n    highlightX,\n    highlightWidth,\n    selectionStartX,\n    selectionEndX,\n    selectionStartIndex,\n    selectionEndIndex,\n    selectionOpacity,\n    panelOpacity,\n    panelEntrance,\n    panelFlipped,\n    dotEntrance,\n    highlightOpacity,\n  };\n\n  return (\n    <View\n      onLayout={onLayout}\n      // Reserve the final height before the first layout (aspectRatio) so\n      // a remount doesn't collapse the chart for a frame.\n      style={[\n        heightProp === undefined\n          ? { width: \"100%\", aspectRatio }\n          : { width: \"100%\", height: heightProp },\n        style,\n      ]}\n    >\n      {width > 0 && height > 0 ? (\n        <GestureDetector gesture={gesture}>\n          <Canvas style={{ width, height }}>\n            {/* Context doesn't cross the Skia reconciler, so re-provide it. */}\n            <ChartContext.Provider value={ctx}>\n              {children}\n            </ChartContext.Provider>\n          </Canvas>\n        </GestureDetector>\n      ) : null}\n    </View>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/charts/time-series-chart.tsx"
    },
    {
      "path": "../native/src/charts/trend-line.tsx",
      "content": "import {\n  DashPathEffect,\n  Group,\n  LinearGradient,\n  Path,\n  Skia,\n  vec,\n} from \"@shopify/react-native-skia\";\nimport { useMemo } from \"react\";\nimport { getDate, getValue, useChart } from \"./chart-context\";\nimport { useEnterOpacity } from \"./fade-mask\";\nimport { DATA_TWEEN_MS } from \"./motion\";\nimport { useMorphPath } from \"./series-paint\";\nimport { withAlpha } from \"./theme\";\n\nexport interface TrendLineProps {\n  dataKey: string;\n  /** \"dashed\" (default, `6,4`), \"solid\" or \"gradient\". */\n  strokeStyle?: \"dashed\" | \"solid\" | \"gradient\";\n  /** Default: theme primary. */\n  stroke?: string;\n  /** Gradient start / end (recipe: chart-3 → chart-1). */\n  gradientStart?: string;\n  gradientEnd?: string;\n  strokeWidth?: number;\n  strokeDasharray?: number[];\n  /** Fade both ends 15% along the line. Default: false. */\n  fadeEdges?: boolean;\n}\n\n/** Least-squares fit of value against time. */\nexport function linearFit(\n  data: Record<string, unknown>[],\n  xKey: string,\n  yKey: string\n): { slope: number; intercept: number } | null {\n  let n = 0;\n  let sx = 0;\n  let sy = 0;\n  let sxx = 0;\n  let sxy = 0;\n  for (const d of data) {\n    const y = getValue(d, yKey);\n    if (y === null) {\n      continue;\n    }\n    const x = getDate(d, xKey).getTime();\n    n++;\n    sx += x;\n    sy += y;\n    sxx += x * x;\n    sxy += x * y;\n  }\n  const denom = n * sxx - sx * sx;\n  if (n < 2 || denom === 0) {\n    return null;\n  }\n  const slope = (n * sxy - sx * sy) / denom;\n  return { slope, intercept: (sy - slope * sx) / n };\n}\n\n/** Regression line across the data (web `TrendLine`). */\nexport function TrendLine({\n  dataKey,\n  strokeStyle = \"dashed\",\n  stroke,\n  gradientStart,\n  gradientEnd,\n  strokeWidth = 2,\n  strokeDasharray = [6, 4],\n  fadeEdges = false,\n}: TrendLineProps) {\n  const { data, xDataKey, xScale, yScale, theme } = useChart();\n  const enter = useEnterOpacity();\n  const color = stroke ?? theme.linePrimary;\n\n  const { target, p0, p1 } = useMemo(() => {\n    const fit = linearFit(data, xDataKey, dataKey);\n    const dates = data.map((d) => getDate(d, xDataKey).getTime());\n    const t0 = Math.min(...dates);\n    const t1 = Math.max(...dates);\n    const b = Skia.PathBuilder.Make();\n    if (!(fit && Number.isFinite(t0))) {\n      return { target: b.build(), p0: vec(0, 0), p1: vec(0, 0) };\n    }\n    const a = vec(xScale(new Date(t0)), yScale(fit.intercept + fit.slope * t0));\n    const z = vec(xScale(new Date(t1)), yScale(fit.intercept + fit.slope * t1));\n    b.moveTo(a.x, a.y);\n    b.lineTo(z.x, z.y);\n    return { target: b.build(), p0: a, p1: z };\n  }, [data, xDataKey, dataKey, xScale, yScale]);\n\n  const path = useMorphPath(target, DATA_TWEEN_MS);\n  const start = gradientStart ?? theme.chart3;\n  const end = gradientEnd ?? color;\n  const gradient = strokeStyle === \"gradient\";\n  const from = gradient ? start : color;\n  const to = gradient ? end : color;\n  const shaded = gradient || fadeEdges;\n\n  return (\n    <Group opacity={enter}>\n      <Path\n        color={color}\n        path={path}\n        strokeCap=\"round\"\n        strokeWidth={strokeWidth}\n        style=\"stroke\"\n      >\n        {strokeStyle === \"dashed\" ? (\n          <DashPathEffect intervals={strokeDasharray} />\n        ) : null}\n        {shaded ? (\n          <LinearGradient\n            colors={\n              fadeEdges\n                ? [withAlpha(from, 0), from, to, withAlpha(to, 0)]\n                : [from, to]\n            }\n            end={p1}\n            positions={fadeEdges ? [0, 0.15, 0.85, 1] : [0, 1]}\n            start={p0}\n          />\n        ) : null}\n      </Path>\n    </Group>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/charts/trend-line.tsx"
    },
    {
      "path": "../native/src/charts/weekends.tsx",
      "content": "import { Group, Rect, Skia } from \"@shopify/react-native-skia\";\nimport { useMemo } from \"react\";\nimport { getDate, useChart } from \"./chart-context\";\nimport { EdgeFade, useEnterOpacity, usePlotRect } from \"./fade-mask\";\nimport { PatternFill, type PatternPreset } from \"./patterns\";\n\nexport interface WeekendsProps {\n  /** \"pattern\" (default) or a flat \"solid\" wash. */\n  variant?: \"pattern\" | \"solid\";\n  pattern?: PatternPreset;\n  /** Default: theme foregroundMuted. */\n  color?: string;\n  /** Default: 0.45 pattern / 0.08 solid (web). */\n  opacity?: number;\n  /** Default: 1.5 (web patternScale). */\n  scale?: number;\n  /** Fade the stripes top and bottom, % of plot height. Default: 0. */\n  fadeVerticalLength?: number;\n}\n\nconst DAY_MS = 86_400_000;\n\n/** Saturday 00:00 → Monday 00:00 spans across the x domain. */\nfunction weekendSpans(start: Date, end: Date): [Date, Date][] {\n  const spans: [Date, Date][] = [];\n  const cursor = new Date(start);\n  cursor.setHours(0, 0, 0, 0);\n  // Back up to this week's Saturday so a span already in progress is kept.\n  cursor.setDate(cursor.getDate() - ((cursor.getDay() + 1) % 7));\n  while (cursor < end) {\n    const sat = new Date(cursor);\n    const mon = new Date(sat.getTime() + 2 * DAY_MS);\n    mon.setHours(0, 0, 0, 0);\n    if (mon > start) {\n      spans.push([sat < start ? start : sat, mon > end ? end : mon]);\n    }\n    cursor.setDate(cursor.getDate() + 7);\n  }\n  return spans;\n}\n\n/** Hatched (or solid) weekend stripes (web `Weekends`). */\nexport function Weekends({\n  variant = \"pattern\",\n  pattern = \"diagonal-up\",\n  color,\n  opacity,\n  scale = 1.5,\n  fadeVerticalLength = 0,\n}: WeekendsProps) {\n  const { data, xDataKey, xScale, theme } = useChart();\n  const rect = usePlotRect();\n  const enter = useEnterOpacity();\n  const tint = color ?? theme.foregroundMuted;\n\n  const stripes = useMemo(() => {\n    if (data.length === 0) {\n      return [];\n    }\n    const [d0, d1] = xScale.domain();\n    return weekendSpans(d0 ?? getDate(data[0] ?? {}, xDataKey), d1 ?? d0).map(\n      ([a, b]) => {\n        const x0 = xScale(a);\n        return { key: a.getTime(), x: x0, width: xScale(b) - x0 };\n      }\n    );\n  }, [data, xDataKey, xScale]);\n\n  const clip = useMemo(() => {\n    const b = Skia.PathBuilder.Make();\n    for (const s of stripes) {\n      b.addRect(Skia.XYWHRect(s.x, rect.y, s.width, rect.height));\n    }\n    return b.build();\n  }, [stripes, rect.y, rect.height]);\n\n  return (\n    <Group opacity={enter}>\n      <EdgeFade\n        bottom={fadeVerticalLength}\n        rect={rect}\n        top={fadeVerticalLength}\n      >\n        {variant === \"solid\" ? (\n          stripes.map((s) => (\n            <Rect\n              color={tint}\n              height={rect.height}\n              key={s.key}\n              opacity={opacity ?? 0.08}\n              width={s.width}\n              x={s.x}\n              y={rect.y}\n            />\n          ))\n        ) : (\n          <PatternFill\n            clip={clip}\n            color={tint}\n            opacity={opacity ?? 0.45}\n            preset={pattern}\n            rect={rect}\n            scale={scale}\n          />\n        )}\n      </EdgeFade>\n    </Group>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/charts/weekends.tsx"
    },
    {
      "path": "../native/src/charts/x-axis-ticks.ts",
      "content": "/**\n * X-axis tick selection, ported verbatim from web `x-axis.tsx`\n * (`selectEvenlySpacedIndices` and helpers) so native picks the same\n * data-aligned ticks: first and last rows always, even on-screen spacing,\n * `targetCount ± 1` tried. Keep in sync until this moves to a shared core.\n */\n\nconst MAX_GAP_LAYOUTS = 400;\n\nfunction binomial(n: number, k: number): number {\n  if (k < 0 || k > n) {\n    return 0;\n  }\n  let result = 1;\n  for (let i = 0; i < k; i++) {\n    result = (result * (n - i)) / (i + 1);\n  }\n  return result;\n}\n\n/** All ways to split `span` into `parts` positive integer gaps. */\nfunction composePositiveSum(sum: number, parts: number): number[][] {\n  if (parts === 1) {\n    return sum >= 1 ? [[sum]] : [];\n  }\n\n  const layouts: number[][] = [];\n  for (let gap = 1; gap <= sum - (parts - 1); gap++) {\n    for (const tail of composePositiveSum(sum - gap, parts - 1)) {\n      layouts.push([gap, ...tail]);\n    }\n  }\n  return layouts;\n}\n\nfunction gapsToIndices(gaps: number[]): number[] {\n  const indices = [0];\n  let position = 0;\n  for (const gap of gaps) {\n    position += gap;\n    indices.push(position);\n  }\n  return indices;\n}\n\nfunction indicesForTickCount(length: number, tickCount: number): number[] {\n  const span = length - 1;\n  if (span <= 0) {\n    return [0];\n  }\n\n  const rawIndices = Array.from({ length: tickCount }, (_, index) =>\n    Math.round((index / (tickCount - 1)) * span)\n  );\n\n  const indices = [...new Set(rawIndices)].sort((a, b) => a - b);\n  if (indices[0] !== 0) {\n    indices.unshift(0);\n  }\n  if (indices.at(-1) !== span) {\n    indices.push(span);\n  }\n\n  return [...new Set(indices)].sort((a, b) => a - b);\n}\n\nfunction allIndexLayouts(length: number, tickCount: number): number[][] {\n  const span = length - 1;\n  if (span <= 0) {\n    return [[0]];\n  }\n\n  const gapCount = tickCount - 1;\n  if (gapCount <= 0) {\n    return [[0]];\n  }\n\n  const layoutCount = binomial(span - 1, gapCount - 1);\n  if (layoutCount > MAX_GAP_LAYOUTS) {\n    return [indicesForTickCount(length, tickCount)];\n  }\n\n  return composePositiveSum(span, gapCount).map(gapsToIndices);\n}\n\nfunction dedupeIndicesByLabel(indices: number[], labels: string[]): number[] {\n  const seenLabels = new Set<string>();\n  const deduped: number[] = [];\n\n  for (const index of indices) {\n    const label = labels[index];\n    if (label === undefined || seenLabels.has(label)) {\n      continue;\n    }\n    seenLabels.add(label);\n    deduped.push(index);\n  }\n\n  return deduped;\n}\n\ninterface TickLayoutScore {\n  score: number;\n  symmetryPenalty: number;\n  countDistance: number;\n  /** 0 = smallest gap at end, 1 = at start, 2 = in the middle */\n  edgePreference: number;\n}\n\nfunction indexGaps(indices: number[]): number[] {\n  const gaps: number[] = [];\n  for (let i = 1; i < indices.length; i++) {\n    const current = indices[i];\n    const previous = indices[i - 1];\n    if (current == null || previous == null) {\n      continue;\n    }\n    gaps.push(current - previous);\n  }\n  return gaps;\n}\n\nfunction smallestGapEdgePreference(indices: number[]): number {\n  const gaps = indexGaps(indices);\n  const smallestGap = Math.min(...gaps);\n  const smallestGapIndex = gaps.indexOf(smallestGap);\n  if (smallestGapIndex === gaps.length - 1) {\n    return 0;\n  }\n  if (smallestGapIndex === 0) {\n    return 1;\n  }\n  return 2;\n}\n\nfunction scoreTickLayout(\n  indices: number[],\n  resolveXPx: (index: number) => number,\n  targetCount: number\n): TickLayoutScore {\n  if (indices.length < 2) {\n    return {\n      score: Number.POSITIVE_INFINITY,\n      symmetryPenalty: Number.POSITIVE_INFINITY,\n      countDistance: Number.POSITIVE_INFINITY,\n      edgePreference: Number.POSITIVE_INFINITY,\n    };\n  }\n\n  const pixelGaps: number[] = [];\n  for (let i = 1; i < indices.length; i++) {\n    const current = indices[i];\n    const previous = indices[i - 1];\n    if (current == null || previous == null) {\n      continue;\n    }\n    pixelGaps.push(resolveXPx(current) - resolveXPx(previous));\n  }\n\n  const minGap = Math.min(...pixelGaps);\n  const maxGap = Math.max(...pixelGaps);\n  const meanGap =\n    pixelGaps.reduce((sum, gap) => sum + gap, 0) / pixelGaps.length;\n  const spreadRatio =\n    meanGap > 0 ? (maxGap - minGap) / meanGap : maxGap - minGap;\n  const countDistance = Math.abs(indices.length - targetCount);\n\n  const gaps = indexGaps(indices);\n  const smallestGap = Math.min(...gaps);\n  const smallestGapIndex = gaps.indexOf(smallestGap);\n  const interiorPenalty =\n    smallestGapIndex > 0 && smallestGapIndex < gaps.length - 1 ? 0.08 : 0;\n\n  const symmetryPenalty =\n    gaps.reduce((penalty, gap, index) => {\n      return penalty + Math.abs(gap - (gaps.at(-1 - index) ?? gap));\n    }, 0) / gaps.length;\n\n  return {\n    score:\n      spreadRatio +\n      0.1 * countDistance +\n      interiorPenalty +\n      symmetryPenalty * 0.02,\n    symmetryPenalty,\n    countDistance,\n    edgePreference: smallestGapEdgePreference(indices),\n  };\n}\n\nfunction isBetterTickLayout(\n  next: TickLayoutScore,\n  best: TickLayoutScore,\n  nextCountDistance: number,\n  bestCountDistance: number\n): boolean {\n  if (next.score < best.score - 1e-6) {\n    return true;\n  }\n  if (Math.abs(next.score - best.score) > 1e-6) {\n    return false;\n  }\n  if (nextCountDistance < bestCountDistance) {\n    return true;\n  }\n  if (nextCountDistance > bestCountDistance) {\n    return false;\n  }\n  if (next.symmetryPenalty < best.symmetryPenalty - 1e-6) {\n    return true;\n  }\n  if (next.symmetryPenalty > best.symmetryPenalty + 1e-6) {\n    return false;\n  }\n  return next.edgePreference < best.edgePreference;\n}\n\n/**\n * Picks tick indices with the most even on-screen spacing. Tries\n * `targetCount ± 1` and evaluates every gap layout when feasible.\n */\nexport function selectEvenlySpacedIndices(\n  length: number,\n  targetCount: number,\n  options?: {\n    labels?: string[];\n    resolveXPx?: (index: number) => number;\n  }\n): number[] {\n  if (length <= 0) {\n    return [];\n  }\n  if (length === 1) {\n    return [0];\n  }\n  if (length <= targetCount) {\n    return Array.from({ length }, (_, index) => index);\n  }\n\n  const resolveXPx = options?.resolveXPx ?? ((index: number) => index);\n\n  const minCount = Math.max(2, targetCount - 1);\n  const maxCount = Math.min(length, targetCount + 1);\n\n  let bestIndices = indicesForTickCount(length, targetCount);\n  let bestScore = scoreTickLayout(bestIndices, resolveXPx, targetCount);\n  let bestCountDistance = bestScore.countDistance;\n\n  for (let tickCount = minCount; tickCount <= maxCount; tickCount++) {\n    for (const rawIndices of allIndexLayouts(length, tickCount)) {\n      const indices = options?.labels\n        ? dedupeIndicesByLabel(rawIndices, options.labels)\n        : rawIndices;\n\n      if (indices.length < 2) {\n        continue;\n      }\n\n      const layoutScore = scoreTickLayout(indices, resolveXPx, targetCount);\n      const countDistance = Math.abs(indices.length - targetCount);\n\n      if (\n        isBetterTickLayout(\n          layoutScore,\n          bestScore,\n          countDistance,\n          bestCountDistance\n        )\n      ) {\n        bestIndices = indices;\n        bestScore = layoutScore;\n        bestCountDistance = countDistance;\n      }\n    }\n  }\n\n  return bestIndices;\n}\n",
      "type": "registry:component",
      "target": "components/charts/x-axis-ticks.ts"
    },
    {
      "path": "../native/src/charts/x-axis.tsx",
      "content": "import { type SkFont, Text } from \"@shopify/react-native-skia\";\nimport { useEffect, useMemo } from \"react\";\nimport {\n  type SharedValue,\n  useAnimatedReaction,\n  useDerivedValue,\n  useSharedValue,\n  withTiming,\n} from \"react-native-reanimated\";\nimport { getDate, useChart } from \"./chart-context\";\nimport {\n  CHART_EASING,\n  DATA_TWEEN_MS,\n  EASE_IN_OUT,\n  HOVER_DIM_MS,\n} from \"./motion\";\nimport { selectEvenlySpacedIndices } from \"./x-axis-ticks\";\n\nexport interface XAxisProps {\n  /** Ticks to aim for, including first and last. Default: 5 (web). */\n  numTicks?: number;\n  /**\n   * \"data\" (default): labels sit on data rows so they line up with the\n   * crosshair. \"domain\": evenly spaced across the time domain.\n   */\n  tickMode?: \"data\" | \"domain\";\n  formatLabel?: (date: Date) => string;\n  color?: string;\n}\n\nconst defaultFormat = (date: Date) =>\n  date.toLocaleDateString(\"en-US\", { month: \"short\", day: \"numeric\" });\n\n/** Labels fade back in over this many px past the date ticker (web: 20). */\nconst FADE_BUFFER = 20;\n/** Keep edge labels this far inside the canvas. */\nconst EDGE_INSET = 4;\n\ninterface Tick {\n  key: number;\n  label: string;\n  /** Centre x in px. */\n  x: number;\n}\n\nfunction dedupe(ticks: Tick[]): Tick[] {\n  const seen = new Set<string>();\n  return ticks.filter((t) => {\n    if (seen.has(t.label)) {\n      return false;\n    }\n    seen.add(t.label);\n    return true;\n  });\n}\n\n/** Same tick rules as web XAxis (data-aligned, or domain when a forecast extends past the data). */\nexport function XAxis({\n  numTicks = 5,\n  tickMode = \"data\",\n  formatLabel = defaultFormat,\n  color,\n}: XAxisProps) {\n  const {\n    data,\n    xDataKey,\n    width,\n    height,\n    margin,\n    xScale,\n    xs,\n    theme,\n    fonts,\n    crosshairX,\n    tooltipOpacity,\n    tickerWidth,\n  } = useChart();\n\n  const ticks = useMemo<Tick[]>(() => {\n    const [d0, d1] = xScale.domain();\n    const last = data.at(-1);\n    const extendsPastData =\n      last !== undefined &&\n      d1 !== undefined &&\n      d1.getTime() > getDate(last, xDataKey).getTime();\n\n    if (tickMode === \"domain\" || extendsPastData) {\n      if (!(d0 && d1)) {\n        return [];\n      }\n      const count = Math.max(2, numTicks);\n      const span = d1.getTime() - d0.getTime();\n      return dedupe(\n        Array.from({ length: count }, (_, i) => {\n          const date = new Date(d0.getTime() + (i / (count - 1)) * span);\n          return {\n            key: date.getTime(),\n            label: formatLabel(date),\n            x: xScale(date),\n          };\n        })\n      );\n    }\n\n    const labels = data.map((d) => formatLabel(getDate(d, xDataKey)));\n    const indices = selectEvenlySpacedIndices(data.length, numTicks, {\n      labels,\n      resolveXPx: (i) => xs[i] ?? i,\n    });\n    return dedupe(\n      indices.map((i) => ({\n        key: getDate(data[i] ?? {}, xDataKey).getTime(),\n        label: labels[i] ?? \"\",\n        x: xs[i] ?? 0,\n      }))\n    );\n  }, [data, xDataKey, xScale, xs, numTicks, tickMode, formatLabel]);\n\n  // Vertically centred on the date ticker pill.\n  const y = height - margin.bottom / 2 + 4;\n\n  return (\n    <>\n      {ticks.map((tick) => (\n        <AxisLabel\n          canvasWidth={width}\n          center={tick.x}\n          color={color ?? theme.label}\n          crosshairX={crosshairX}\n          font={fonts.axis}\n          key={tick.key}\n          text={tick.label}\n          tickerWidth={tickerWidth}\n          tooltipOpacity={tooltipOpacity}\n          y={y}\n        />\n      ))}\n    </>\n  );\n}\n\nfunction AxisLabel({\n  text,\n  center,\n  canvasWidth,\n  y,\n  font,\n  color,\n  crosshairX,\n  tooltipOpacity,\n  tickerWidth,\n}: {\n  text: string;\n  center: number;\n  canvasWidth: number;\n  y: number;\n  font: SkFont;\n  color: string;\n  crosshairX: SharedValue<number>;\n  tooltipOpacity: SharedValue<number>;\n  tickerWidth: SharedValue<number>;\n}) {\n  const w = Math.ceil(font.getTextWidth(text));\n  // Centred on its point, nudged inside the canvas at the edges.\n  const targetX = Math.min(\n    Math.max(center - w / 2, EDGE_INSET),\n    canvasWidth - w - EDGE_INSET\n  );\n\n  // Web: `left` tweens over the y-domain duration when ticks move.\n  const x = useSharedValue(targetX);\n  useEffect(() => {\n    x.value = withTiming(targetX, {\n      duration: DATA_TWEEN_MS,\n      easing: CHART_EASING,\n    });\n  }, [x, targetX]);\n  const mid = useDerivedValue(() => x.value + w / 2);\n\n  // Hidden under the date ticker, fading back in just beyond it. Web eases\n  // label opacity with `opacity 0.4s ease-in-out`.\n  const target = useDerivedValue(() => {\n    const half = tickerWidth.value / 2;\n    if (half === 0 || tooltipOpacity.value === 0) {\n      return 1;\n    }\n    const distance = Math.abs(mid.value - crosshairX.value);\n    if (distance < half) {\n      return 0;\n    }\n    if (distance < half + FADE_BUFFER) {\n      return (distance - half) / FADE_BUFFER;\n    }\n    return 1;\n  });\n  const opacity = useSharedValue(1);\n  useAnimatedReaction(\n    () => target.value,\n    (next, prev) => {\n      if (prev !== null && next !== prev) {\n        opacity.value = withTiming(next, {\n          duration: HOVER_DIM_MS,\n          easing: EASE_IN_OUT,\n        });\n      }\n    }\n  );\n\n  return (\n    <Text color={color} font={font} opacity={opacity} text={text} x={x} y={y} />\n  );\n}\n",
      "type": "registry:component",
      "target": "components/charts/x-axis.tsx"
    }
  ]
}