{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sparkline",
  "type": "registry:ui",
  "title": "Sparkline",
  "description": "Show a compact trend beside a value.",
  "categories": [
    "data"
  ],
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://irisui.dev/r/foundation.json",
    "https://irisui.dev/r/motion-tokens.json",
    "https://irisui.dev/r/utils.json"
  ],
  "files": [
    {
      "path": "src/registry/components/sparkline/sparkline.tsx",
      "content": "\"use client\";\n\nimport {\n  useCallback,\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n  type KeyboardEvent,\n  type PointerEvent,\n} from \"react\";\nimport {\n  AnimatePresence,\n  animate,\n  motion,\n  useInView,\n  useMotionValue,\n  useMotionValueEvent,\n  useReducedMotion,\n  useTransform,\n  type Variants,\n} from \"motion/react\";\nimport { motionTokens } from \"../lib/motion-tokens\";\nimport { cn } from \"../lib/utils\";\n\nexport interface SparklineProps {\n  data: number[];\n  label: string;\n  value?: string;\n  change?: string;\n  tone?: \"accent\" | \"success\" | \"warning\" | \"danger\";\n  width?: number;\n  height?: number;\n  /** One label per point, such as a date. It replaces the change while that point is scrubbed. */\n  labels?: string[];\n  /** Formats a scrubbed point for the headline. Defaults to a grouped number. */\n  formatValue?: (value: number, index: number) => string;\n  /** A quiet neutral fill under the line. */\n  area?: boolean;\n  /** Pointer and keyboard scrubbing through the points. */\n  interactive?: boolean;\n}\n\n/** Copy that holds a number enters from the side it moved toward: a larger value rises from below, a smaller one drops from above. */\nconst rise: Variants = {\n  hidden: (direction: number) => ({\n    opacity: 0,\n    y: `${0.3 * direction}em`,\n    filter: `blur(${motionTokens.blur.soft}px)`,\n  }),\n  shown: {\n    opacity: 1,\n    y: 0,\n    filter: \"blur(0px)\",\n    transition: { duration: motionTokens.duration.standard, ease: [...motionTokens.ease.enter] },\n  },\n  gone: (direction: number) => ({\n    opacity: 0,\n    y: `${-0.3 * direction}em`,\n    filter: `blur(${motionTokens.blur.subtle}px)`,\n    transition: { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.standard] },\n  }),\n};\nconst fade: Variants = {\n  hidden: { opacity: 0, y: 0, filter: \"blur(0px)\" },\n  shown: { opacity: 1, y: 0, filter: \"blur(0px)\", transition: { duration: motionTokens.duration.instant } },\n  gone: { opacity: 0, y: 0, filter: \"blur(0px)\", transition: { duration: motionTokens.duration.instant } },\n};\nconst amountIn = (text: string) =>\n  Number(\n    text\n      .replace(/,/g, \"\")\n      .replace(/−/g, \"-\")\n      .match(/-?\\d+(?:\\.\\d+)?/)?.[0] ?? NaN,\n  );\nconst grouped = new Intl.NumberFormat(\"en-US\");\n/** The first draw: one smooth stroke, slow at both ends like a pen. */\nconst draw = { duration: motionTokens.duration.considered * 1.75, ease: [...motionTokens.ease.inOut] } as const;\nconst quick = { duration: motionTokens.duration.fast, ease: [...motionTokens.ease.standard] } as const;\n\n/** Tone colours: `text` for copy and the area wash, `stroke` for the line and the end dot. */\nconst toneText = {\n  accent: \"text-accent-ink\",\n  success: \"text-green\",\n  warning: \"text-orange\",\n  danger: \"text-red\",\n} as const;\nconst toneStroke = {\n  accent: \"stroke-accent\",\n  success: \"stroke-green\",\n  warning: \"stroke-orange\",\n  danger: \"stroke-red\",\n} as const;\n\n/** Splits text into user-perceived characters, so a digit or letter never splits into code points. */\nconst graphemes = (text: string) => Array.from(new Intl.Segmenter().segment(text), (part) => part.segment);\n\ntype Chunk = { slot: string; key: string; text: string };\n/** A number keeps its unchanged digits still, keyed by place from the right. Words key on everything up to them, so only the first changed word and what follows it roll. */\nfunction chunksOf(text: string, whole: boolean): Chunk[] {\n  if (!text) return [];\n  if (whole) return [{ slot: \"all\", key: text, text }];\n  if (!/\\p{L}/u.test(text))\n    return graphemes(text).map((char, index, all) => ({ slot: `n${all.length - index}`, key: char, text: char }));\n  const words = text.split(/(?<=\\s)/);\n  return words.map((word, index) => ({ slot: `w${index}`, key: words.slice(0, index + 1).join(\"\"), text: word }));\n}\n\n/** Text that changes rolls in place, like a numeric content transition; `morph` springs the wrapper to the new width so nothing beside it jumps. */\nfunction Roll({\n  text,\n  direction,\n  whole = false,\n  morph = false,\n  tone,\n}: {\n  text: string;\n  direction?: number;\n  whole?: boolean;\n  morph?: boolean;\n  tone?: string;\n}) {\n  const reduceMotion = !!useReducedMotion();\n  const sizer = useRef<HTMLSpanElement>(null);\n  const width = useMotionValue<number | \"auto\">(\"auto\");\n  const [shown, setShown] = useState({ text, direction: 1 });\n  if (shown.text !== text) setShown({ text, direction: direction ?? (amountIn(text) < amountIn(shown.text) ? -1 : 1) });\n  useEffect(() => {\n    const node = sizer.current;\n    if (!node || typeof ResizeObserver === \"undefined\") return;\n    let measured: string | null = null;\n    // Layout size, not the transformed rect, so a scaling parent never leaves the text clipped. Only a new text springs; font loads jump.\n    const observer = new ResizeObserver(([entry]) => {\n      if (!entry) return;\n      const next = entry.borderBoxSize?.[0]?.inlineSize ?? node.offsetWidth;\n      if (measured !== null && measured !== node.textContent && !reduceMotion)\n        animate(width, next, motionTokens.spring.morph);\n      else width.jump(next);\n      measured = node.textContent;\n    });\n    observer.observe(node);\n    return () => observer.disconnect();\n  }, [morph, reduceMotion, width]);\n  const numeric = !whole && !/\\p{L}/u.test(text);\n  const variants = reduceMotion ? fade : rise;\n  const anchor = numeric ? \"right\" : \"left\";\n  const piece = cn(\n    \"relative inline-block whitespace-pre\",\n    whole && \"max-w-full overflow-hidden text-ellipsis align-top\",\n  );\n  return (\n    <motion.span\n      className=\"relative inline-flex max-w-full overflow-x-clip whitespace-pre align-top data-end:justify-end\"\n      data-end={numeric || undefined}\n      data-whole={whole || undefined}\n      style={morph ? { width } : undefined}\n    >\n      {morph && (\n        <span\n          ref={sizer}\n          className=\"pointer-events-none invisible absolute top-0 left-0 whitespace-pre\"\n          aria-hidden=\"true\"\n        >\n          {text}\n        </span>\n      )}\n      {/* One flex item holds the pieces, so they stay inline and the text reads and copies as one string. */}\n      <span className={piece}>\n        <AnimatePresence mode=\"popLayout\" initial={false} custom={shown.direction} anchorX={anchor}>\n          {chunksOf(text, whole).map((chunk) => (\n            <motion.span\n              key={chunk.slot}\n              className={piece}\n              custom={shown.direction}\n              variants={variants}\n              initial=\"hidden\"\n              animate=\"shown\"\n              exit=\"gone\"\n            >\n              <AnimatePresence mode=\"popLayout\" initial={false} custom={shown.direction} anchorX={anchor}>\n                <motion.span\n                  key={`${tone ?? \"\"}${chunk.key}`}\n                  className={cn(piece, tone)}\n                  custom={shown.direction}\n                  variants={variants}\n                  initial=\"hidden\"\n                  animate=\"shown\"\n                  exit=\"gone\"\n                >\n                  {chunk.text}\n                </motion.span>\n              </AnimatePresence>\n            </motion.span>\n          ))}\n        </AnimatePresence>\n      </span>\n    </motion.span>\n  );\n}\n\ntype Point = [number, number];\n/** Space kept above the highest and below the lowest point, and half the line width. */\nconst inset = 5,\n  cap = 1.25;\nconst grid = Array.from({ length: 161 }, (_, index) => index / 160);\n\n/** A smooth line through every point, sampled densely in unit space (x and y both 0 to 1, y up). Monotone cubic tangents keep it from swinging past the data. */\nfunction curveFor(data: number[]): Point[] {\n  const min = Math.min(...data),\n    span = Math.max(...data) - min;\n  const ys = data.map((value) => (span ? (value - min) / span : 0.5));\n  const last = ys.length - 1,\n    step = 1 / last;\n  const slopes = ys.slice(1).map((y, index) => (y - ys[index]!) / step);\n  const tangents = ys.map((_, index) =>\n    index === 0 || index === last\n      ? slopes[0]!\n      : (Math.sign(slopes[index - 1]!) + Math.sign(slopes[index]!)) *\n          Math.min(\n            Math.abs(slopes[index - 1]!),\n            Math.abs(slopes[index]!),\n            Math.abs(slopes[index - 1]! + slopes[index]!) / 4,\n          ) || 0,\n  );\n  if (last > 1) {\n    tangents[0] = (3 * slopes[0]! - tangents[1]!) / 2;\n    tangents[last] = (3 * slopes[last - 1]! - tangents[last - 1]!) / 2;\n  }\n  const samples = Math.max(1, Math.min(16, Math.round(128 / last)));\n  const points: Point[] = [];\n  for (let index = 0; index < last; index++)\n    for (let sample = 0; sample < samples; sample++) {\n      const t = sample / samples,\n        t2 = t * t,\n        t3 = t2 * t;\n      const y =\n        (2 * t3 - 3 * t2 + 1) * ys[index]! +\n        (t3 - 2 * t2 + t) * step * tangents[index]! +\n        (3 * t2 - 2 * t3) * ys[index + 1]! +\n        (t3 - t2) * step * tangents[index + 1]!;\n      points.push([(index + t) * step, Math.min(1, Math.max(0, y))]);\n    }\n  points.push([1, ys[last]!]);\n  return points;\n}\n\n/** Height of the sampled line at x, straight between samples. */\nfunction heightAt(shape: Point[], x: number) {\n  let low = 0,\n    high = shape.length - 1;\n  if (x <= shape[low]![0]) return shape[low]![1];\n  if (x >= shape[high]![0]) return shape[high]![1];\n  while (high - low > 1) {\n    const middle = (low + high) >> 1;\n    if (shape[middle]![0] < x) low = middle;\n    else high = middle;\n  }\n  const [x0, y0] = shape[low]!,\n    [x1, y1] = shape[high]!;\n  return x1 === x0 ? y1 : y0 + ((y1 - y0) * (x - x0)) / (x1 - x0);\n}\n\n/** Every drawn string for one frame, in the chart's own pixels so strokes and dots stay round at any width. */\nfunction geometryOf(shape: Point[], w: number, h: number, at: number) {\n  const toY = (y: number) => inset + (1 - y) * (h - inset * 2);\n  const points = shape.map(([x, y]): Point => [x * w, toY(y)]);\n  const pairs = points.map(([x, y]) => `${x.toFixed(2)},${y.toFixed(2)}`);\n  const cx = at * w,\n    cy = toY(heightAt(shape, at));\n  return {\n    points,\n    line: pairs.join(\" \"),\n    trail: `M${pairs.join(\"L\")}`,\n    fill: `M${pairs.join(\"L\")}L${w},${h}L0,${h}Z`,\n    dot: `${cx},${cy} ${cx},${cy}`,\n    cx,\n  };\n}\n\n/** Where the pen is along the line once `progress` of its length is drawn, so the fill follows the tip. */\nfunction tipAt(points: Point[], progress: number) {\n  const steps = points.slice(1).map(([x, y], index) => Math.hypot(x - points[index]![0], y - points[index]![1]));\n  let left = progress * steps.reduce((sum, step) => sum + step, 0);\n  for (let index = 0; index < steps.length; index++) {\n    const step = steps[index]!,\n      from = points[index]![0];\n    if (left <= step) return from + (points[index + 1]![0] - from) * (step ? left / step : 1);\n    left -= step;\n  }\n  return points[points.length - 1]![0];\n}\n\nexport function Sparkline({\n  data,\n  label,\n  value,\n  change,\n  tone = \"accent\",\n  width = 160,\n  height = 52,\n  labels,\n  formatValue,\n  area = true,\n  interactive = true,\n}: SparklineProps) {\n  const safeData = data.length > 1 ? data : [data[0] ?? 0, data[0] ?? 0];\n  const last = safeData.length - 1;\n  const key = safeData.join(\",\");\n  const target = useMemo(() => curveFor(key.split(\",\").map(Number)), [key]);\n  const description = `${label}: ${value ?? safeData[last]}. ${change ?? \"\"}`.trim();\n  const uid = `sparkline${useId().replace(/[^a-zA-Z0-9_-]/g, \"\")}`;\n  const ref = useRef<HTMLElement>(null);\n  const plot = useRef<HTMLDivElement>(null);\n  const inView = useInView(ref, { once: true, amount: 0.5 });\n  const reduceMotion = !!useReducedMotion();\n  const [box, setBox] = useState({ w: width, h: height });\n  const [active, setActive] = useState<number | null>(null);\n  const [landed, setLanded] = useState(false);\n  const index = active === null ? null : Math.min(active, last);\n  const [moved, setMoved] = useState({ index, direction: 1 });\n  if (moved.index !== index)\n    setMoved({ index, direction: index === null || moved.index === null ? 1 : Math.sign(index - moved.index) || 1 });\n\n  const [first] = useState(() => geometryOf(target, width, height, 1));\n  const shape = useRef(target);\n  const size = useRef(box);\n  const drawn = useMotionValue(0),\n    cursor = useMotionValue(1);\n  const line = useMotionValue(first.line),\n    trail = useMotionValue(first.trail),\n    fill = useMotionValue(first.fill),\n    dot = useMotionValue(first.dot);\n  const cursorX = useMotionValue(first.cx),\n    lit = useMotionValue(first.cx + cap + 16),\n    dim = useMotionValue(first.cx + cap),\n    reveal = useMotionValue(0);\n  const lineOpacity = useTransform(drawn, (progress) => (progress > 0.001 ? 1 : 0));\n\n  // One writer for every drawn attribute: the morphing shape, the cursor, the pen and the measured size all repaint through here.\n  const paint = useCallback(() => {\n    const { w, h } = size.current,\n      progress = drawn.get();\n    const next = geometryOf(shape.current, w, h, cursor.get());\n    line.set(next.line);\n    trail.set(next.trail);\n    fill.set(next.fill);\n    dot.set(next.dot);\n    cursorX.set(next.cx);\n    lit.set(next.cx + cap + 16);\n    dim.set(next.cx + cap);\n    reveal.set(progress >= 1 ? w + 32 : tipAt(next.points, progress) + 16);\n  }, [cursor, cursorX, dim, dot, drawn, fill, line, lit, reveal, trail]);\n  useMotionValueEvent(cursor, \"change\", paint);\n  useMotionValueEvent(drawn, \"change\", paint);\n\n  // The chart draws in its own pixels: measure the width, keep the height in proportion.\n  useLayoutEffect(() => {\n    const node = plot.current;\n    if (!node || typeof ResizeObserver === \"undefined\") return;\n    const observer = new ResizeObserver(() => {\n      const w = node.clientWidth;\n      if (w) setBox((current) => (Math.abs(current.w - w) < 0.5 ? current : { w, h: (w * height) / width }));\n    });\n    observer.observe(node);\n    return () => observer.disconnect();\n  }, [height, width]);\n  useLayoutEffect(() => {\n    size.current = box;\n    paint();\n  }, [box, paint]);\n\n  // The line draws left to right once it is seen, the fill follows the pen, then the end dot lands.\n  useEffect(() => {\n    // Reduced motion shows the finished line at once; the dot lands a frame later, after hydration has matched the server.\n    if (reduceMotion) {\n      drawn.jump(1);\n      const frame = requestAnimationFrame(() => setLanded(true));\n      return () => cancelAnimationFrame(frame);\n    }\n    if (!inView) return;\n    const controls = animate(drawn, 1, { ...draw, onComplete: () => setLanded(true) });\n    return () => controls.stop();\n  }, [drawn, inView, reduceMotion]);\n\n  // New data morphs the line from the shape on screen, sampled on shared x positions so a series that gains or loses points still flows.\n  useEffect(() => {\n    const from = shape.current;\n    if (from === target) return;\n    if (reduceMotion) {\n      shape.current = target;\n      paint();\n      return;\n    }\n    const xs = [...new Set([...grid, ...target.map(([x]) => x)])].sort((a, b) => a - b);\n    const start = xs.map((x) => heightAt(from, x)),\n      goal = xs.map((x) => heightAt(target, x));\n    const controls = animate(0, 1, {\n      ...motionTokens.spring.smooth,\n      onUpdate: (progress) => {\n        shape.current = xs.map((x, at): Point => [x, start[at]! + (goal[at]! - start[at]!) * progress]);\n        paint();\n      },\n      onComplete: () => {\n        shape.current = target;\n        paint();\n      },\n    });\n    return () => controls.stop();\n  }, [paint, reduceMotion, target]);\n\n  // The cursor springs to the scrubbed point and rides the line between points; leaving sends it home to the latest value.\n  useEffect(() => {\n    const to = index === null ? 1 : index / last;\n    if (reduceMotion) {\n      cursor.jump(to);\n      return;\n    }\n    const controls = animate(cursor, to, motionTokens.spring.snappy);\n    return () => controls.stop();\n  }, [cursor, index, last, reduceMotion]);\n\n  const scrubbing = index !== null;\n  const format = (at: number) => (formatValue ? formatValue(safeData[at]!, at) : grouped.format(safeData[at]!));\n  const point = format(index ?? last),\n    when = labels?.[index ?? last];\n  const headline = value === undefined ? undefined : scrubbing ? point : value;\n  const aside =\n    index === null\n      ? (change ?? \"\")\n      : value === undefined\n        ? [point, when].filter(Boolean).join(\" · \")\n        : (when ?? `${index + 1} of ${last + 1}`);\n  const pointAt = (clientX: number) => {\n    const rect = plot.current?.getBoundingClientRect();\n    return rect?.width ? Math.max(0, Math.min(last, Math.round(((clientX - rect.left) / rect.width) * last))) : last;\n  };\n  const onKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {\n    if (event.key === \"Escape\" && scrubbing) {\n      event.preventDefault();\n      setActive(null);\n      return;\n    }\n    const from = index ?? last,\n      page = Math.max(1, Math.round((last + 1) / 6));\n    const next = (\n      {\n        ArrowLeft: from - 1,\n        ArrowDown: from - 1,\n        ArrowRight: from + 1,\n        ArrowUp: from + 1,\n        PageDown: from - page,\n        PageUp: from + page,\n        Home: 0,\n        End: last,\n      } as Record<string, number>\n    )[event.key];\n    if (next === undefined) return;\n    event.preventDefault();\n    setActive(Math.max(0, Math.min(last, next)));\n  };\n  const scrub = interactive\n    ? {\n        role: \"slider\",\n        tabIndex: 0,\n        \"aria-label\": `${label}, explore values`,\n        \"aria-valuemin\": 1,\n        \"aria-valuemax\": last + 1,\n        \"aria-valuenow\": (index ?? last) + 1,\n        \"aria-valuetext\": `${when ?? (index === null ? \"Latest\" : `Point ${index + 1} of ${last + 1}`)}: ${point}`,\n        onPointerDown: (event: PointerEvent<HTMLDivElement>) => setActive(pointAt(event.clientX)),\n        onPointerMove: (event: PointerEvent<HTMLDivElement>) => {\n          if (event.pointerType === \"mouse\" || event.buttons) setActive(pointAt(event.clientX));\n        },\n        onPointerUp: (event: PointerEvent<HTMLDivElement>) => {\n          if (event.pointerType !== \"mouse\") setActive(null);\n        },\n        onPointerLeave: () => setActive(null),\n        onPointerCancel: () => setActive(null),\n        onBlur: () => setActive(null),\n        onKeyDown,\n      }\n    : {};\n  return (\n    <figure ref={ref} className=\"m-0 grid min-w-0 gap-3 text-ink\" aria-label={description}>\n      <figcaption className=\"flex min-w-0 items-baseline gap-2 text-(length:--text-sm) text-ink-2\">\n        <span className=\"min-w-0 overflow-hidden whitespace-nowrap\">\n          <Roll text={label} whole />\n        </span>\n        {headline !== undefined && (\n          <strong className=\"ml-auto text-(length:--text-lg) font-medium tracking-body text-ink tabular-nums\">\n            <Roll text={headline} morph />\n          </strong>\n        )}\n        {/* The scrubbed date is context, not a change, so it stays neutral whatever the tone. */}\n        <small\n          className={cn(\n            \"flex-none text-(length:--text-xs) font-medium tabular-nums transition-[color] duration-standard ease-standard motion-reduce:transition-none\",\n            toneText[tone],\n          )}\n        >\n          <Roll\n            text={aside}\n            direction={scrubbing ? moved.direction : undefined}\n            tone={scrubbing ? \"text-ink-2\" : undefined}\n            morph\n          />\n        </small>\n      </figcaption>\n      {/* The plot is the scrubber: hover or drag across it, or focus it and use the arrow keys. Vertical swipes still scroll the page. */}\n      <div\n        ref={plot}\n        className=\"relative min-w-0 rounded-[calc(var(--radius-control)/2)] outline-none [-webkit-tap-highlight-color:transparent] focus-visible:[--focus-outline-offset:8px] data-interactive:cursor-default data-interactive:touch-pan-y data-interactive:select-none\"\n        data-interactive={interactive || undefined}\n        {...scrub}\n      >\n        <svg\n          className=\"block h-auto w-full overflow-visible\"\n          viewBox={`0 0 ${box.w} ${box.h}`}\n          aria-hidden=\"true\"\n          focusable=\"false\"\n        >\n          <defs>\n            <clipPath id={`${uid}-lit`} clipPathUnits=\"userSpaceOnUse\">\n              <motion.rect x={-16} y={-16} width={lit} height={box.h + 32} />\n            </clipPath>\n            <clipPath id={`${uid}-dim`} clipPathUnits=\"userSpaceOnUse\">\n              <motion.rect x={dim} y={-16} width={box.w + 32} height={box.h + 32} />\n            </clipPath>\n            <clipPath id={`${uid}-fill`} clipPathUnits=\"userSpaceOnUse\">\n              <motion.rect x={-16} y={-16} width={reveal} height={box.h + 32} />\n            </clipPath>\n          </defs>\n          {/* The area under the line carries a faint wash of the line tone. */}\n          {area && (\n            <motion.path\n              className={cn(\"fill-[color-mix(in_oklab,currentColor_9%,transparent)] stroke-none\", toneText[tone])}\n              d={fill}\n              clipPath={`url(#${uid}-fill)`}\n            />\n          )}\n          <line className=\"stroke-line [stroke-width:1]\" x1=\"0\" y1={box.h - 0.5} x2={box.w} y2={box.h - 0.5} />\n          {/* While scrubbing, the stretch after the cursor dims, as if the later points have not happened yet. */}\n          <motion.path\n            className=\"fill-none stroke-line-strong [stroke-width:2.5] [stroke-linecap:round] [stroke-linejoin:round]\"\n            d={trail}\n            clipPath={`url(#${uid}-dim)`}\n            initial={false}\n            animate={{ opacity: scrubbing && landed ? 1 : 0 }}\n            transition={reduceMotion ? { duration: 0 } : quick}\n          />\n          <motion.polyline\n            className={cn(\n              \"fill-none [stroke-width:2.5] [stroke-linecap:round] [stroke-linejoin:round] transition-[stroke] duration-standard ease-standard motion-reduce:transition-none\",\n              toneStroke[tone],\n            )}\n            points={line}\n            clipPath={`url(#${uid}-lit)`}\n            style={{ pathLength: drawn, opacity: lineOpacity }}\n          />\n          <motion.line\n            className=\"stroke-line-strong [stroke-width:1]\"\n            x1={cursorX}\n            x2={cursorX}\n            y1={0}\n            y2={box.h}\n            initial={false}\n            animate={{ opacity: scrubbing ? 1 : 0 }}\n            transition={reduceMotion ? { duration: 0 } : quick}\n          />\n          {/* A zero-length round-capped stroke is a true circle, and it takes the line's colour. It rests on the latest point and becomes the scrub handle. */}\n          <motion.polyline\n            className={cn(\n              \"transition-[stroke] duration-standard ease-standard motion-reduce:transition-none\",\n              toneStroke[tone],\n            )}\n            points={dot}\n            initial={false}\n            animate={{ opacity: landed ? 1 : 0, strokeWidth: landed ? (scrubbing ? 10 : 7) : 0 }}\n            transition={reduceMotion ? { duration: 0 } : { opacity: quick, strokeWidth: motionTokens.spring.snappy }}\n            fill=\"none\"\n            strokeLinecap=\"round\"\n          />\n        </svg>\n      </div>\n    </figure>\n  );\n}\n\nexport default Sparkline;\n",
      "type": "registry:component",
      "target": "@components/iris/sparkline/sparkline.tsx"
    }
  ],
  "docs": "Docs and live preview: https://irisui.dev/components/sparkline",
  "meta": {
    "kind": "component",
    "group": "Charts",
    "export": "Sparkline",
    "tags": [
      "data",
      "chart"
    ],
    "docs": "https://irisui.dev/components/sparkline"
  }
}
