{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sparkline",
  "title": "Sparkline",
  "description": "A word-sized line chart that draws in once, rings its newest point in the surface colour and morphs to new values from the shape on screen. Optional scrub readout.",
  "registryDependencies": [
    "https://eased.dev/r/eased-tokens.json",
    "https://eased.dev/r/eased-chart-core.json"
  ],
  "files": [
    {
      "path": "src/registry/eased/sparkline/sparkline.tsx",
      "content": "\"use client\";\n\nimport { animate, motion, useMotionValue, useTransform } from \"motion/react\";\nimport { useCallback, useId, useLayoutEffect, useMemo, useRef } from \"react\";\nimport { snappy, useSafeReducedMotion } from \"../lib/motion\";\nimport { useDrawIn } from \"../_primitives/chart-core/draw-in\";\nimport { samplesToArea, samplesToPath, useMorphPath, type Curve, type MorphFrame } from \"../_primitives/chart-core/morph\";\nimport { linear, type Domain } from \"../_primitives/chart-core/scales\";\nimport { ChartTooltip, Crosshair, useActiveStore, useChartScrub } from \"../_primitives/chart-core/scrub\";\nimport { useChartTransition } from \"../_primitives/chart-core/range\";\nimport { useChartSize } from \"../_primitives/chart-core/use-chart-size\";\nimport { ChartTokens, chartColor } from \"../_primitives/chart-core/tokens\";\n\nexport type SparklineProps = {\n  /** The values, oldest first, evenly spaced. */\n  values: readonly number[];\n  /** Accessible name, e.g. \"Revenue, last 30 days\". A summary (first, last, low, high) is appended. */\n  label: string;\n  /** Stroke colour. Default `var(--eased-chart-1)`. Use `chartColor(n)` or any token. */\n  color?: string;\n  /** Height in px. Default 32. The width follows the container. */\n  height?: number;\n  /** Add a soft wash under the line. Default false. */\n  area?: boolean;\n  /** Mark the newest value with a dot ringed in the surface colour. Default true. */\n  dot?: boolean;\n  /** Default \"monotone\". */\n  curve?: Curve;\n  /** Include zero in the value range (honest for counts and money). Default false: a sparkline shows shape. */\n  zero?: boolean;\n  /** Scrub to read values: a crosshair and a small readout, and the sparkline becomes a slider. Default false. */\n  interactive?: boolean;\n  /** Formats values for the readout and screen readers. */\n  format?: (value: number) => string;\n  /** The label of point `i` for the readout and screen readers, e.g. its date. */\n  formatIndex?: (index: number) => string;\n  onActiveChange?: (index: number | null) => void;\n  className?: string;\n};\n\nconst PAD = 5; // dot radius + ring: the newest point never clips\nconst fallbackFormat = (v: number) => new Intl.NumberFormat(undefined, { maximumFractionDigits: 1 }).format(v);\n\nfunction extent(values: readonly number[], zero: boolean): Domain {\n  let lo = Infinity;\n  let hi = -Infinity;\n  for (const v of values) {\n    if (v < lo) lo = v;\n    if (v > hi) hi = v;\n  }\n  if (lo === Infinity) return [0, 1];\n  if (zero) [lo, hi] = [Math.min(0, lo), Math.max(0, hi)];\n  if (lo === hi) return [lo - 1, hi + 1];\n  return [lo, hi];\n}\n\n/**\n * A word-sized line chart. It draws in left to right the first time it scrolls into view,\n * then the newest point settles in as a dot ringed in the surface colour. New values morph\n * the line from the shape on screen, so it can sit in a Stat Tile that changes range.\n */\nexport function Sparkline({\n  values,\n  label,\n  color = chartColor(1),\n  height = 32,\n  area = false,\n  dot = true,\n  curve = \"monotone\",\n  zero = false,\n  interactive = false,\n  format = fallbackFormat,\n  formatIndex,\n  onActiveChange,\n  className = \"\",\n}: SparklineProps) {\n  const reduced = useSafeReducedMotion();\n  const transition = useChartTransition();\n  const [box, { width }] = useChartSize();\n  const id = useId().replace(/:/g, \"\");\n  const line = useRef<SVGPathElement>(null);\n  const fill = useRef<SVGPathElement>(null);\n  const end = useRef<SVGCircleElement>(null);\n  const progress = useDrawIn(box, { reduced });\n  const widthMV = useMotionValue(width);\n  useLayoutEffect(() => widthMV.set(width), [width, widthMV]);\n  const clipWidth = useTransform([progress, widthMV], ([p, w]: number[]) => p * (w + PAD * 2));\n  const dotOpacity = useTransform(progress, [0.88, 1], [0, 1]);\n  const dotScale = useTransform(progress, [0.88, 1], [0.5, 1]);\n\n  const domain = useMemo(() => extent(values, zero), [values, zero]);\n  const plotW = Math.max(0, width - PAD);\n\n  const draw = useCallback(\n    (f: MorphFrame) => {\n      const s = f.series.v;\n      if (!s) return;\n      const x = (u: number) => u * plotW;\n      const y = linear(f.domain, [height - PAD, PAD]);\n      line.current?.setAttribute(\"d\", samplesToPath(s, x, y));\n      fill.current?.setAttribute(\"d\", samplesToArea(s, x, y, height));\n      if (end.current) {\n        end.current.setAttribute(\"cx\", String(x(1)));\n        end.current.setAttribute(\"cy\", String(y(s[s.length - 1])));\n      }\n    },\n    [plotW, height],\n  );\n  const morph = useMorphPath(\n    { series: { v: values }, domain },\n    { onFrame: draw, curve, transition, animate: !reduced, samples: Math.min(240, Math.max(64, values.length * 12)) },\n  );\n  useLayoutEffect(() => morph.redraw(), [draw, morph]);\n\n  // Scrubbing (opt-in).\n  const store = useActiveStore();\n  const n = values.length;\n  const xOf = useCallback((i: number) => (n > 1 ? (i / (n - 1)) * plotW : plotW), [n, plotW]);\n  const indexAt = useCallback((px: number) => (n > 1 ? Math.round((px / Math.max(1, plotW)) * (n - 1)) : 0), [n, plotW]);\n  const valueText = useCallback((i: number) => `${formatIndex ? `${formatIndex(i)}: ` : \"\"}${format(values[i] ?? 0)}`, [format, formatIndex, values]);\n  const activeY = useMotionValue(0);\n  const scrub = useChartScrub({\n    count: n,\n    store,\n    xOf,\n    indexAt,\n    valueText,\n    label,\n    reduced,\n    onActiveChange: (i) => {\n      if (i !== null) {\n        const y = linear(domain, [height - PAD, PAD])(values[i]);\n        if (reduced || scrub.opacity.get() < 0.01) activeY.jump(y);\n        else animate(activeY, y, snappy);\n      }\n      onActiveChange?.(i);\n    },\n  });\n\n  const summary = n\n    ? `${format(values[0])} to ${format(values[n - 1])}, low ${format(Math.min(...values))}, high ${format(Math.max(...values))}`\n    : \"no data\";\n\n  return (\n    <div\n      ref={box}\n      className={`relative w-full min-w-0 ${className}`}\n      style={{ height }}\n      {...(interactive ? {} : { role: \"img\", \"aria-label\": `${label}: ${summary}` })}\n    >\n      <ChartTokens />\n      {width > 0 && (\n        <svg aria-hidden width={width} height={height} className=\"absolute inset-0 overflow-visible\">\n          <defs>\n            <clipPath id={`${id}-clip`}>\n              <motion.rect x={-PAD} y={-PAD} height={height + PAD * 2} width={clipWidth} />\n            </clipPath>\n            <linearGradient id={`${id}-wash`} x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n              <stop offset=\"0\" stopColor={color} stopOpacity={0.18} />\n              <stop offset=\"1\" stopColor={color} stopOpacity={0} />\n            </linearGradient>\n          </defs>\n          <g clipPath={`url(#${id}-clip)`}>\n            {area && <path ref={fill} fill={`url(#${id}-wash)`} />}\n            <path ref={line} fill=\"none\" stroke={color} strokeWidth={1.5} strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n          </g>\n          {interactive && (\n            <>\n              <Crosshair x={scrub.x} opacity={scrub.opacity} height={height} />\n              <motion.circle\n                r={3}\n                cx={0}\n                fill={color}\n                stroke=\"var(--chart-surface, var(--eased-surface))\"\n                strokeWidth={2}\n                paintOrder=\"stroke\"\n                style={{ x: scrub.x, y: activeY, opacity: scrub.opacity }}\n              />\n            </>\n          )}\n          {dot && n > 0 && (\n            <motion.circle\n              ref={end}\n              r={3}\n              fill={color}\n              stroke=\"var(--chart-surface, var(--eased-surface))\"\n              strokeWidth={2}\n              paintOrder=\"stroke\"\n              style={{ opacity: dotOpacity, scale: dotScale, transformBox: \"fill-box\", transformOrigin: \"center\" }}\n            />\n          )}\n        </svg>\n      )}\n      {interactive && (\n        <>\n          <div {...scrub.props} className=\"absolute inset-0 cursor-crosshair rounded-control-xs focus-ring\" />\n          <ChartTooltip store={store} xOf={xOf} width={width} top={-38} reduced={reduced} className=\"whitespace-nowrap py-1.5\">\n            {(i) => (\n              <span className=\"flex items-baseline gap-2\">\n                {formatIndex && <span className=\"text-text-muted\">{formatIndex(i)}</span>}\n                <span className=\"font-medium tabular-nums\">{format(values[i] ?? 0)}</span>\n              </span>\n            )}\n          </ChartTooltip>\n        </>\n      )}\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@components/eased/sparkline/sparkline.tsx"
    }
  ],
  "docs": "Import Sparkline from components/eased/sparkline/sparkline. The Eased tokens were merged into your global CSS; nothing to import. Dark mode follows .dark or data-theme=\"dark\".",
  "type": "registry:ui"
}