{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "line-chart",
  "title": "Line chart",
  "description": "interactive/sparkline · `area` · `onPointerChange`",
  "dependencies": [
    "react-native-svg@15.15.4"
  ],
  "registryDependencies": [
    "@kit-native/lib-palette-context"
  ],
  "files": [
    {
      "path": "components/kit/line-chart.tsx",
      "content": "import { usePalette } from '@/lib/palette-context';\nimport * as React from 'react';\nimport { type LayoutChangeEvent, PanResponder, View } from 'react-native';\nimport Svg, { Circle, Defs, Line, LinearGradient, Path, Stop } from 'react-native-svg';\n\nexport type LinePoint = { x: number; y: number };\nexport type ChartTone = 'primary' | 'success' | 'destructive' | 'warning' | 'info' | 'foreground';\n\ntype Props = {\n  /** number[] → x = index. */\n  data: number[] | LinePoint[];\n  /** `sparkline` = no interaction; `interactive` = touch crosshair. */\n  variant?: 'sparkline' | 'interactive';\n  height?: number;\n  width?: number;\n  tone?: ChartTone;\n  strokeWidth?: number;\n  area?: boolean;\n  /** Faint horizontal guides at 25/50/75%. Default true for interactive. */\n  grid?: boolean;\n  domain?: { min: number; max: number };\n  onPointerChange?: (point: LinePoint | null) => void;\n};\n\nfunction toPoints(data: number[] | LinePoint[]): LinePoint[] {\n  if (data.length === 0) return [];\n  return typeof data[0] === 'number' ? (data as number[]).map((y, x) => ({ x, y })) : (data as LinePoint[]);\n}\n\n/** SVG line chart. Colours come from usePalette() (the one place hex is allowed: SVG props). */\nexport function LineChart({ data, variant = 'interactive', height = 160, width: widthProp, tone = 'primary', strokeWidth = 2.5, area = false, grid, domain, onPointerChange }: Props) {\n  const colors = usePalette();\n  const color = colors[tone];\n  const points = React.useMemo(() => toPoints(data), [data]);\n  const [measuredW, setMeasuredW] = React.useState(widthProp ?? 0);\n  const [hoverIdx, setHoverIdx] = React.useState<number | null>(null);\n  const W = widthProp ?? measuredW;\n  const H = height;\n  const gradId = React.useId().replace(/:/g, '');\n\n  const { yMin, yMax } = React.useMemo(() => {\n    if (domain) return { yMin: domain.min, yMax: domain.max };\n    if (points.length === 0) return { yMin: 0, yMax: 1 };\n    let mn = points[0].y;\n    let mx = points[0].y;\n    for (const p of points) {\n      mn = Math.min(mn, p.y);\n      mx = Math.max(mx, p.y);\n    }\n    const span = mx - mn || 1;\n    return { yMin: mn - span * 0.06, yMax: mx + span * 0.06 };\n  }, [points, domain]);\n\n  const { xMin, xMax } = React.useMemo(() => {\n    if (points.length === 0) return { xMin: 0, xMax: 1 };\n    let mn = points[0].x;\n    let mx = points[0].x;\n    for (const p of points) {\n      mn = Math.min(mn, p.x);\n      mx = Math.max(mx, p.x);\n    }\n    return { xMin: mn, xMax: mn === mx ? mn + 1 : mx };\n  }, [points]);\n\n  const sx = React.useCallback((xv: number) => 1 + ((xv - xMin) / (xMax - xMin)) * (W - 2), [xMin, xMax, W]);\n  const sy = React.useCallback((yv: number) => H - ((yv - yMin) / (yMax - yMin)) * H, [yMin, yMax, H]);\n\n  const pathD = React.useMemo(() => {\n    if (points.length === 0 || W === 0) return '';\n    return points.map((p, i) => `${i === 0 ? 'M' : 'L'} ${sx(p.x).toFixed(2)} ${sy(p.y).toFixed(2)}`).join(' ');\n  }, [points, W, sx, sy]);\n\n  const areaD = React.useMemo(() => {\n    if (!area || !pathD) return '';\n    return `${pathD} L ${sx(points[points.length - 1].x).toFixed(2)} ${H} L ${sx(points[0].x).toFixed(2)} ${H} Z`;\n  }, [area, pathD, points, sx, H]);\n\n  const latest = React.useRef({ variant, points, W, xMin, xMax, hoverIdx, onPointerChange });\n  React.useEffect(() => {\n    latest.current = { variant, points, W, xMin, xMax, hoverIdx, onPointerChange };\n  });\n\n  const responder = React.useMemo(() => {\n    const handleMove = (locationX: number) => {\n      const L = latest.current;\n      if (L.variant !== 'interactive' || L.points.length === 0 || L.W === 0) return;\n      const dataX = L.xMin + (locationX / L.W) * (L.xMax - L.xMin);\n      let nearest = 0;\n      let best = Number.POSITIVE_INFINITY;\n      L.points.forEach((p, i) => {\n        const d = Math.abs(p.x - dataX);\n        if (d < best) {\n          best = d;\n          nearest = i;\n        }\n      });\n      if (nearest !== L.hoverIdx) {\n        setHoverIdx(nearest);\n        L.onPointerChange?.(L.points[nearest]);\n      }\n    };\n    const release = () => {\n      setHoverIdx(null);\n      latest.current.onPointerChange?.(null);\n    };\n    // Gesture callbacks run at event time, not during render; the ref reads inside are safe.\n    // eslint-disable-next-line react-hooks/refs\n    return PanResponder.create({\n      onStartShouldSetPanResponder: () => latest.current.variant === 'interactive',\n      onMoveShouldSetPanResponder: () => latest.current.variant === 'interactive',\n      onPanResponderGrant: (e) => handleMove(e.nativeEvent.locationX),\n      onPanResponderMove: (e) => handleMove(e.nativeEvent.locationX),\n      onPanResponderRelease: release,\n      onPanResponderTerminate: release,\n    });\n  }, []);\n\n  const hoverPt = hoverIdx != null ? points[hoverIdx] : null;\n\n  return (\n    <View\n      onLayout={(e: LayoutChangeEvent) => {\n        if (!widthProp) setMeasuredW(e.nativeEvent.layout.width);\n      }}\n      {...(variant === 'interactive' ? responder.panHandlers : {})}\n      style={{ width: widthProp ?? '100%', height: H }}>\n      {W > 0 && points.length > 0 ? (\n        <Svg width={W} height={H}>\n          {(grid ?? variant === 'interactive')\n            ? [0.25, 0.5, 0.75].map((f) => <Line key={f} x1={0} x2={W} y1={H * f} y2={H * f} stroke={colors.border} strokeWidth={1} strokeDasharray=\"2 4\" />)\n            : null}\n          {area ? (\n            <Defs>\n              <LinearGradient id={gradId} x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n                <Stop offset=\"0\" stopColor={color} stopOpacity={0.28} />\n                <Stop offset=\"1\" stopColor={color} stopOpacity={0} />\n              </LinearGradient>\n            </Defs>\n          ) : null}\n          {area ? <Path d={areaD} fill={`url(#${gradId})`} /> : null}\n          <Path d={pathD} fill=\"none\" stroke={color} strokeWidth={strokeWidth} strokeLinejoin=\"round\" strokeLinecap=\"round\" />\n          {hoverPt ? (\n            <>\n              <Line x1={sx(hoverPt.x)} x2={sx(hoverPt.x)} y1={0} y2={H} stroke={colors.mutedForeground} strokeWidth={1} strokeDasharray=\"3 3\" opacity={0.6} />\n              <Circle cx={sx(hoverPt.x)} cy={sy(hoverPt.y)} r={5} fill={color} stroke={colors.background} strokeWidth={2} />\n            </>\n          ) : null}\n        </Svg>\n      ) : null}\n    </View>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/kit/line-chart.tsx"
    }
  ],
  "meta": {
    "exports": [
      "LineChart"
    ],
    "api": "<LineChart data variant=\"interactive|sparkline\" height tone area? onPointerChange? />"
  },
  "docs": "Run `npx expo install --fix` afterwards so Expo SDK packages match your SDK. Needs the kit theme (`@kit-native/theme`) and `KitThemeProvider` from `@kit-native/lib-theme-context` at the root.",
  "categories": [
    "data"
  ],
  "type": "registry:component"
}