{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sparkline",
  "type": "registry:component",
  "title": "Sparkline",
  "description": "Tiny inline line, area, and bar charts for tables, KPI cards, and dense dashboards, with hover sync and a shared resize observer",
  "dependencies": [
    "d3-shape",
    "motion"
  ],
  "registryDependencies": [
    "@bklit/chart-context",
    "@bklit/chart-animation",
    "@bklit/utils"
  ],
  "files": [
    {
      "path": "src/charts/sparkline.tsx",
      "content": "\"use client\";\n\nimport {\n  AnimatePresence,\n  animate as animateValue,\n  type MotionValue,\n  motion,\n  useReducedMotion,\n  useSpring,\n} from \"motion/react\";\nimport {\n  type CSSProperties,\n  memo,\n  type PointerEvent,\n  type ReactNode,\n  type RefObject,\n  useCallback,\n  useEffect,\n  useId,\n  useLayoutEffect,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { DEFAULT_ANIMATION_EASING } from \"./animation\";\nimport { chartCssVars } from \"./chart-context\";\nimport {\n  buildSparklineAreaPath,\n  buildSparklineBarSegment,\n  buildSparklineBarsPath,\n  buildSparklineLinePath,\n  computeSparklineDomain,\n  computeSparklineLayout,\n  describeSparkline,\n  interpolateSparklineLayout,\n  resolveSparklinePadding,\n  resolveSparklineValues,\n  type SparklineCurve,\n  type SparklineDataKey,\n  type SparklineLayout,\n  type SparklinePadding,\n  sparklineBarWidth,\n  sparklineIndexAtX,\n} from \"./sparkline-geometry\";\n\nexport const DEFAULT_SPARKLINE_ANIMATION_DURATION_MS = 900;\n/** Matches the main charts' data-change tween (`DEFAULT_Y_DOMAIN_TWEEN_MS`). */\nexport const DEFAULT_SPARKLINE_DATA_TRANSITION_MS = 500;\nconst DATA_TRANSITION_EASE = [0.85, 0, 0.15, 1] as const;\n/** Same as the main charts' `SeriesHoverDim` / `HighlightSegment`. */\nconst HOVER_FADE = \"opacity 400ms ease-in-out\";\nconst HIGHLIGHT_SPRING: SparklineSpringConfig = { stiffness: 180, damping: 28 };\nconst DEFAULT_SPARKLINE_HEIGHT = 32;\nconst REDUCED_MOTION_FADE_MS = 200;\n\nexport type SparklineCrosshair = \"dashed\" | \"solid\" | \"gradient\" | false;\n\nexport interface SparklineSpringConfig {\n  stiffness: number;\n  damping: number;\n}\n\n/**\n * Same feel as the main charts' tooltip crosshair (`tooltipSpring`). Inlined\n * so the sparkline install doesn't pull in the tooltip.\n */\nexport const DEFAULT_SPARKLINE_HOVER_SPRING: SparklineSpringConfig = {\n  stiffness: 300,\n  damping: 30,\n};\n\nexport interface SparklineBaseProps<T> {\n  data: readonly T[];\n  /** Field (or accessor) holding the numeric value. */\n  dataKey: SparklineDataKey<T>;\n  /** Fixed width in px. Omit to fill the parent width. */\n  width?: number;\n  /** Height in px. Defaults to 32. */\n  height?: number;\n  /** Series colour. Defaults to `var(--chart-line-primary)`. */\n  color?: string;\n  /** Inner padding so strokes and the hover dot are not clipped. */\n  padding?: number | Partial<SparklinePadding>;\n  /** Override the value domain; `undefined` entries fall back to the data. */\n  yDomain?: [number?, number?];\n  /** Play the enter animation on mount. Respects `prefers-reduced-motion`. */\n  animate?: boolean;\n  animationDuration?: number;\n  /** Any CSS easing string. */\n  animationEasing?: string;\n  /**\n   * Morph to new data instead of snapping, in ms. `0` snaps. Requires\n   * `animate`; snaps under `prefers-reduced-motion`.\n   */\n  dataTransitionDuration?: number;\n  /** Hover guide line. Set `false` to hide. */\n  crosshair?: SparklineCrosshair;\n  /** Show a dot on the hovered point. */\n  dot?: boolean;\n  /**\n   * Spring the crosshair and dot between points, like the main charts'\n   * tooltip. Snaps under `prefers-reduced-motion`.\n   */\n  hoverTransition?: boolean;\n  /**\n   * Hover spring for the crosshair and dot. Defaults to the main charts'\n   * tooltip spring (300 / 30); pass only what you want to change, e.g.\n   * `{ damping: 40 }`. Higher stiffness = snappier; lower damping = bouncier.\n   */\n  springConfig?: Partial<SparklineSpringConfig>;\n  /** Disable pointer tracking entirely (no hover state, no listeners). */\n  interactive?: boolean;\n  /** Controlled hovered index. Pass `null` for \"not hovering\". */\n  hoverIndex?: number | null;\n  onHoverChange?: (index: number | null, datum: T | null) => void;\n  /** Accessible summary. Generated from the data when omitted. */\n  \"aria-label\"?: string;\n  className?: string;\n  style?: CSSProperties;\n}\n\nexport interface LineSparklineProps<T> extends SparklineBaseProps<T> {\n  curve?: SparklineCurve;\n  strokeWidth?: number;\n  /** Keep a dot on the last point when not hovering. */\n  endDot?: boolean;\n  /**\n   * On hover, fade the series and re-stroke the segment around the hovered\n   * point at full strength — the main charts' highlight. Default `true`.\n   */\n  showHighlight?: boolean;\n  /** Series opacity while hovering (with `showHighlight`). Default `0.5`. */\n  inactiveOpacity?: number;\n}\n\nexport interface AreaSparklineProps<T> extends LineSparklineProps<T> {\n  fill?: \"gradient\" | \"solid\";\n  /** Opacity of the fill at its strongest point. */\n  fillOpacity?: number;\n}\n\nexport interface BarSparklineProps<T> extends SparklineBaseProps<T> {\n  /** Gap between bars in px. */\n  barGap?: number;\n  barRadius?: number;\n  /** Opacity of non-hovered bars while hovering. */\n  inactiveOpacity?: number;\n}\n\n// One ResizeObserver for every sparkline on the page.\ntype SizeListener = (width: number) => void;\nlet sharedObserver: ResizeObserver | null = null;\nconst sizeListeners = new WeakMap<Element, SizeListener>();\n\nfunction observeWidth(element: Element, listener: SizeListener) {\n  if (typeof ResizeObserver === \"undefined\") {\n    listener(element.getBoundingClientRect().width);\n    return () => undefined;\n  }\n  sharedObserver ??= new ResizeObserver((entries) => {\n    for (const entry of entries) {\n      sizeListeners.get(entry.target)?.(entry.contentRect.width);\n    }\n  });\n  sizeListeners.set(element, listener);\n  sharedObserver.observe(element);\n  return () => {\n    sizeListeners.delete(element);\n    sharedObserver?.unobserve(element);\n  };\n}\n\nfunction useSparklineWidth(\n  ref: RefObject<HTMLDivElement | null>,\n  fixedWidth: number | undefined\n): number {\n  const [measured, setMeasured] = useState(0);\n  useEffect(() => {\n    const element = ref.current;\n    if (fixedWidth !== undefined || !element) {\n      return;\n    }\n    return observeWidth(element, (width) =>\n      setMeasured(Math.round(width * 2) / 2)\n    );\n  }, [ref, fixedWidth]);\n  return fixedWidth ?? measured;\n}\n\nfunction prefersReducedMotion(): boolean {\n  return (\n    typeof window !== \"undefined\" &&\n    window.matchMedia?.(\"(prefers-reduced-motion: reduce)\").matches === true\n  );\n}\n\n/**\n * Plays the enter animation once, when the sparkline first has a size.\n * Uses WAAPI so there is no per-instance animation runtime; under reduced\n * motion the reveal becomes a short opacity fade.\n */\nfunction useEnterAnimation(\n  ref: RefObject<SVGGElement | null>,\n  ready: boolean,\n  options: {\n    animate: boolean;\n    duration: number;\n    easing: string;\n    keyframes: Keyframe[];\n  }\n) {\n  const played = useRef(false);\n  const { animate, duration, easing, keyframes } = options;\n  useLayoutEffect(() => {\n    const element = ref.current;\n    if (!(ready && element) || played.current) {\n      return;\n    }\n    played.current = true;\n    if (!animate || duration <= 0 || typeof element.animate !== \"function\") {\n      return;\n    }\n    if (prefersReducedMotion()) {\n      element.animate([{ opacity: 0 }, { opacity: 1 }], {\n        duration: REDUCED_MOTION_FADE_MS,\n        easing: \"ease-out\",\n      });\n      return;\n    }\n    element.animate(keyframes, { duration, easing });\n  }, [ref, ready, animate, duration, easing, keyframes]);\n}\n\n/** True when two layouts draw the same shape (e.g. a re-created `data` array). */\nfunction sameShape(a: SparklineLayout, b: SparklineLayout): boolean {\n  if (a === b) {\n    return true;\n  }\n  if (a.points.length !== b.points.length || a.baselineY !== b.baselineY) {\n    return false;\n  }\n  return a.points.every((point, i) => {\n    const other = b.points[i];\n    return (\n      other !== undefined && Object.is(point.y, other.y) && point.x === other.x\n    );\n  });\n}\n\n/**\n * Morphs the path(s) when the data changes, writing `d` straight to the DOM\n * each frame so React doesn't re-render. First paint and resizes snap.\n */\nfunction useDataTransition(\n  layout: SparklineLayout,\n  width: number,\n  duration: number,\n  draw: (frame: SparklineLayout) => void\n) {\n  const displayed = useRef<SparklineLayout | null>(null);\n  const prevWidth = useRef(width);\n  const running = useRef(false);\n  const drawRef = useRef(draw);\n  useLayoutEffect(() => {\n    drawRef.current = draw;\n  });\n\n  useLayoutEffect(() => {\n    const from = displayed.current;\n    const resized = prevWidth.current !== width;\n    prevWidth.current = width;\n    displayed.current = layout;\n    if (\n      !from ||\n      sameShape(from, layout) ||\n      resized ||\n      duration <= 0 ||\n      prefersReducedMotion()\n    ) {\n      // An interrupted tween may have left the DOM mid-frame.\n      if (running.current) {\n        running.current = false;\n        drawRef.current(layout);\n      }\n      return;\n    }\n    const frame = (t: number) => {\n      displayed.current = interpolateSparklineLayout(from, layout, t);\n      drawRef.current(displayed.current);\n    };\n    running.current = true;\n    frame(0);\n    const control = animateValue(0, 1, {\n      duration: duration / 1000,\n      ease: [...DATA_TRANSITION_EASE],\n      onUpdate: frame,\n      onComplete: () => {\n        running.current = false;\n        displayed.current = layout;\n        drawRef.current(layout);\n      },\n    });\n    return () => control.stop();\n  }, [layout, width, duration]);\n}\n\nconst wipeKeyframes: Keyframe[] = [\n  { clipPath: \"inset(0 100% 0 0)\" },\n  { clipPath: \"inset(0 0% 0 0)\" },\n];\n\nconst drawKeyframes: Keyframe[] = [\n  { strokeDashoffset: 1 },\n  { strokeDashoffset: 0 },\n];\n\nfunction useHoverState<T>(\n  data: readonly T[],\n  hoverIndexProp: number | null | undefined,\n  onHoverChange: SparklineBaseProps<T>[\"onHoverChange\"]\n) {\n  const [internal, setInternal] = useState<number | null>(null);\n  const controlled = hoverIndexProp !== undefined;\n  const hoverIndex = controlled ? hoverIndexProp : internal;\n  const current = useRef(hoverIndex);\n  current.current = hoverIndex;\n\n  const setHoverIndex = useCallback(\n    (index: number | null) => {\n      // Pointer moves inside one slot are free: no state, no callback.\n      if (index === current.current) {\n        return;\n      }\n      current.current = index;\n      if (!controlled) {\n        setInternal(index);\n      }\n      onHoverChange?.(index, index === null ? null : (data[index] ?? null));\n    },\n    [controlled, data, onHoverChange]\n  );\n\n  return [hoverIndex, setHoverIndex] as const;\n}\n\ninterface FrameProps<T> {\n  config: SparklineBaseProps<T>;\n  kind: \"point\" | \"band\";\n  includeZero: boolean;\n  defaultPadding: number;\n  children: (frame: {\n    width: number;\n    height: number;\n    layout: SparklineLayout;\n    values: number[];\n    hoverIndex: number | null;\n    color: string;\n    gradientId: string;\n  }) => ReactNode;\n}\n\nfunction SparklineFrame<T>({\n  config,\n  kind,\n  includeZero,\n  defaultPadding,\n  children,\n}: FrameProps<T>) {\n  const {\n    data,\n    dataKey,\n    width: widthProp,\n    height = DEFAULT_SPARKLINE_HEIGHT,\n    color = chartCssVars.linePrimary,\n    padding: paddingProp,\n    yDomain,\n    interactive = true,\n    hoverIndex: hoverIndexProp,\n    onHoverChange,\n    className,\n    style,\n  } = config;\n  const containerRef = useRef<HTMLDivElement>(null);\n  const width = useSparklineWidth(containerRef, widthProp);\n  const gradientId = useId();\n\n  const values = useMemo(\n    () => resolveSparklineValues(data, dataKey),\n    [data, dataKey]\n  );\n  const resolvedPadding = resolveSparklinePadding(paddingProp, defaultPadding);\n  const { top, right, bottom, left } = resolvedPadding;\n  // Keyed on the four numbers so inline `padding={{ … }}` literals stay cheap.\n  const padding = useMemo(\n    () => ({ top, right, bottom, left }),\n    [top, right, bottom, left]\n  );\n  const yMin = yDomain?.[0];\n  const yMax = yDomain?.[1];\n  const layout = useMemo(\n    () =>\n      computeSparklineLayout(values, {\n        width,\n        height,\n        padding,\n        kind,\n        domain: computeSparklineDomain(values, {\n          yDomain: [yMin, yMax],\n          includeZero,\n        }),\n      }),\n    [values, width, height, padding, kind, yMin, yMax, includeZero]\n  );\n\n  const [hoverIndex, setHoverIndex] = useHoverState(\n    data,\n    hoverIndexProp,\n    onHoverChange\n  );\n\n  const handlePointerMove = useCallback(\n    (event: PointerEvent<SVGSVGElement>) => {\n      const rect = event.currentTarget.getBoundingClientRect();\n      setHoverIndex(\n        sparklineIndexAtX(event.clientX - rect.left, {\n          count: values.length,\n          width,\n          padding,\n          kind,\n        })\n      );\n    },\n    [setHoverIndex, values.length, width, padding, kind]\n  );\n  const handlePointerLeave = useCallback(\n    () => setHoverIndex(null),\n    [setHoverIndex]\n  );\n\n  const label = config[\"aria-label\"] ?? describeSparkline(values);\n\n  return (\n    <div\n      className={cn(\n        \"relative min-w-0\",\n        widthProp === undefined && \"w-full\",\n        className\n      )}\n      ref={containerRef}\n      style={{ width: widthProp, height, ...style }}\n    >\n      {width > 0 ? (\n        <svg\n          aria-label={label}\n          className=\"block overflow-visible\"\n          height={height}\n          onPointerLeave={interactive ? handlePointerLeave : undefined}\n          onPointerMove={interactive ? handlePointerMove : undefined}\n          role=\"img\"\n          style={interactive ? { touchAction: \"pan-y\" } : undefined}\n          width={width}\n        >\n          {children({\n            width,\n            height,\n            layout,\n            values,\n            hoverIndex,\n            color,\n            gradientId,\n          })}\n        </svg>\n      ) : null}\n    </div>\n  );\n}\n\ntype AnimatedNumber = number | MotionValue<number>;\n\nfunction SparklineCrosshairLine({\n  x,\n  height,\n  padding,\n  variant,\n  gradientId,\n}: {\n  x: AnimatedNumber;\n  height: number;\n  padding: SparklinePadding;\n  variant: Exclude<SparklineCrosshair, false>;\n  gradientId: string;\n}) {\n  const id = `${gradientId}-crosshair`;\n  return (\n    <motion.g aria-hidden pointerEvents=\"none\" style={{ x }}>\n      {variant === \"gradient\" ? (\n        <>\n          <defs>\n            <linearGradient id={id} x1=\"0\" x2=\"0\" y1=\"0\" y2=\"1\">\n              <stop\n                offset=\"0\"\n                stopColor={chartCssVars.crosshair}\n                stopOpacity=\"0\"\n              />\n              <stop\n                offset=\"0.5\"\n                stopColor={chartCssVars.crosshair}\n                stopOpacity=\"1\"\n              />\n              <stop\n                offset=\"1\"\n                stopColor={chartCssVars.crosshair}\n                stopOpacity=\"0\"\n              />\n            </linearGradient>\n          </defs>\n          {/* Gradient strokes need a non-zero bbox width, so draw a 1px rect. */}\n          <rect fill={`url(#${id})`} height={height} width={1} x={-0.5} y={0} />\n        </>\n      ) : (\n        <line\n          stroke={chartCssVars.crosshair}\n          strokeDasharray={variant === \"dashed\" ? \"2 2\" : undefined}\n          strokeWidth={1}\n          x1={0}\n          x2={0}\n          y1={padding.top / 2}\n          y2={height - padding.bottom / 2}\n        />\n      )}\n    </motion.g>\n  );\n}\n\nfunction SparklineDot({\n  x,\n  y,\n  color,\n}: {\n  x: AnimatedNumber;\n  y: AnimatedNumber;\n  color: string;\n}) {\n  return (\n    <motion.circle\n      aria-hidden\n      cx={x}\n      cy={y}\n      fill={color}\n      pointerEvents=\"none\"\n      r={3}\n      stroke={chartCssVars.background}\n      strokeWidth={1.5}\n    />\n  );\n}\n\ninterface HoverMarksProps {\n  point: SparklineLayout[\"points\"][number];\n  /** Where the dot rests when the hovered value is missing. */\n  fallbackY: number;\n  height: number;\n  padding: SparklinePadding;\n  crosshair: SparklineCrosshair;\n  dot: boolean;\n  color: string;\n  gradientId: string;\n}\n\nfunction HoverMarksView({\n  x,\n  y,\n  showDot,\n  height,\n  padding,\n  crosshair,\n  dot,\n  color,\n  gradientId,\n}: Omit<HoverMarksProps, \"point\" | \"fallbackY\"> & {\n  x: AnimatedNumber;\n  y: AnimatedNumber;\n  showDot: boolean;\n}) {\n  return (\n    <>\n      {crosshair ? (\n        <SparklineCrosshairLine\n          gradientId={gradientId}\n          height={height}\n          padding={padding}\n          variant={crosshair}\n          x={x}\n        />\n      ) : null}\n      {dot && showDot ? <SparklineDot color={color} x={x} y={y} /> : null}\n    </>\n  );\n}\n\n/**\n * Crosshair + dot follow the pointer on the same spring as the main charts'\n * tooltip. Only mounted while hovering, so idle sparklines create no springs;\n * the springs start at the first hovered point rather than sliding in.\n */\nfunction SpringHoverMarks({\n  point,\n  fallbackY,\n  spring,\n  ...rest\n}: HoverMarksProps & { spring: SparklineSpringConfig }) {\n  const targetY = Number.isFinite(point.y) ? point.y : fallbackY;\n  const x = useSpring(point.x, spring);\n  const y = useSpring(targetY, spring);\n  useLayoutEffect(() => {\n    x.set(point.x);\n    y.set(targetY);\n  }, [x, y, point.x, targetY]);\n  return (\n    <HoverMarksView showDot={Number.isFinite(point.y)} x={x} y={y} {...rest} />\n  );\n}\n\nfunction HoverMarks({\n  spring,\n  point,\n  fallbackY,\n  ...rest\n}: HoverMarksProps & { spring: SparklineSpringConfig | null }) {\n  const reducedMotion = useReducedMotion();\n  if (spring && !reducedMotion) {\n    return (\n      <SpringHoverMarks\n        fallbackY={fallbackY}\n        point={point}\n        spring={spring}\n        {...rest}\n      />\n    );\n  }\n  return (\n    <HoverMarksView\n      showDot={Number.isFinite(point.y)}\n      x={point.x}\n      y={point.y}\n      {...rest}\n    />\n  );\n}\n\n/**\n * Re-strokes the series path clipped to a band from the previous to the next\n * point; the band springs as the hovered index moves. Only mounted while\n * hovering, so idle sparklines carry no springs.\n */\nfunction SparklineHighlight({\n  layout,\n  hoverIndex,\n  height,\n  lineRef,\n  stroke,\n  strokeWidth,\n}: {\n  layout: SparklineLayout;\n  hoverIndex: number;\n  height: number;\n  lineRef: RefObject<SVGPathElement | null>;\n  stroke: string;\n  strokeWidth: number;\n}) {\n  const clipId = useId();\n  const highlightRef = useRef<SVGPathElement>(null);\n  const { points } = layout;\n  const start = points[Math.max(0, hoverIndex - 1)]?.x ?? 0;\n  const end = points[Math.min(points.length - 1, hoverIndex + 1)]?.x ?? 0;\n  const x = useSpring(start, HIGHLIGHT_SPRING);\n  const width = useSpring(end - start, HIGHLIGHT_SPRING);\n\n  useLayoutEffect(() => {\n    // Reuse the base stroke's `d` — it may be mid data-transition.\n    highlightRef.current?.setAttribute(\n      \"d\",\n      lineRef.current?.getAttribute(\"d\") ?? \"\"\n    );\n    if (prefersReducedMotion()) {\n      x.jump(start);\n      width.jump(end - start);\n    } else {\n      x.set(start);\n      width.set(end - start);\n    }\n  });\n\n  return (\n    <g aria-hidden pointerEvents=\"none\">\n      <defs>\n        <clipPath id={clipId}>\n          {/* Bleed by strokeWidth so strokes at the min/max keep full width. */}\n          <motion.rect\n            height={height + strokeWidth * 2}\n            width={width}\n            x={x}\n            y={-strokeWidth}\n          />\n        </clipPath>\n      </defs>\n      <motion.path\n        animate={{ opacity: 1 }}\n        clipPath={`url(#${clipId})`}\n        fill=\"none\"\n        initial={{ opacity: 0 }}\n        ref={highlightRef}\n        stroke={stroke}\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        strokeWidth={strokeWidth}\n        transition={{ duration: 0.4, ease: \"easeInOut\" }}\n      />\n    </g>\n  );\n}\n\nfunction HoverOverlay({\n  layout,\n  hoverIndex,\n  spring,\n  ...rest\n}: Omit<HoverMarksProps, \"point\" | \"fallbackY\"> & {\n  layout: SparklineLayout;\n  hoverIndex: number | null;\n  spring: SparklineSpringConfig | null;\n}) {\n  const point = hoverIndex === null ? undefined : layout.points[hoverIndex];\n  if (!point) {\n    return null;\n  }\n  return (\n    <HoverMarks\n      fallbackY={layout.baselineY}\n      point={point}\n      spring={spring}\n      {...rest}\n    />\n  );\n}\n\n/** Resolves the hover spring: `null` means snap. */\nfunction resolveHoverSpring(\n  hoverTransition: boolean,\n  springConfig: Partial<SparklineSpringConfig> | undefined\n): SparklineSpringConfig | null {\n  return hoverTransition\n    ? { ...DEFAULT_SPARKLINE_HOVER_SPRING, ...springConfig }\n    : null;\n}\n\nfunction LineOrArea<T>({\n  config,\n  variant,\n}: {\n  config: AreaSparklineProps<T>;\n  variant: \"line\" | \"area\";\n}) {\n  const {\n    curve = \"monotone\",\n    strokeWidth = 1.5,\n    endDot = false,\n    fill = \"gradient\",\n    fillOpacity = variant === \"area\" ? 0.3 : 0,\n    animate = true,\n    animationDuration = DEFAULT_SPARKLINE_ANIMATION_DURATION_MS,\n    animationEasing = DEFAULT_ANIMATION_EASING,\n    dataTransitionDuration = DEFAULT_SPARKLINE_DATA_TRANSITION_MS,\n    crosshair = \"dashed\",\n    dot = true,\n    hoverTransition = true,\n    springConfig,\n    showHighlight = true,\n    inactiveOpacity = 0.5,\n  } = config;\n  const spring = resolveHoverSpring(hoverTransition, springConfig);\n  const padding = resolveSparklinePadding(config.padding, 4);\n  const groupRef = useRef<SVGGElement>(null);\n  const lineRef = useRef<SVGPathElement>(null);\n\n  return (\n    <SparklineFrame\n      config={config}\n      defaultPadding={4}\n      includeZero={false}\n      kind=\"point\"\n    >\n      {({ width, height, layout, hoverIndex, color, gradientId }) => (\n        <LineOrAreaPaths\n          animate={animate}\n          animationDuration={animationDuration}\n          animationEasing={animationEasing}\n          color={color}\n          crosshair={crosshair}\n          curve={curve}\n          dataTransitionDuration={animate ? dataTransitionDuration : 0}\n          dot={dot}\n          endDot={endDot}\n          fill={fill}\n          fillOpacity={fillOpacity}\n          gradientId={gradientId}\n          groupRef={groupRef}\n          height={height}\n          hoverIndex={hoverIndex}\n          inactiveOpacity={showHighlight ? inactiveOpacity : 1}\n          layout={layout}\n          lineRef={lineRef}\n          padding={padding}\n          showHighlight={showHighlight}\n          spring={spring}\n          strokeWidth={strokeWidth}\n          variant={variant}\n          width={width}\n        />\n      )}\n    </SparklineFrame>\n  );\n}\n\nfunction LineOrAreaPaths({\n  variant,\n  layout,\n  width,\n  height,\n  padding,\n  curve,\n  strokeWidth,\n  color,\n  fill,\n  fillOpacity,\n  endDot,\n  hoverIndex,\n  crosshair,\n  dot,\n  gradientId,\n  animate,\n  animationDuration,\n  animationEasing,\n  dataTransitionDuration,\n  groupRef,\n  lineRef,\n  spring,\n  showHighlight,\n  inactiveOpacity,\n}: {\n  variant: \"line\" | \"area\";\n  layout: SparklineLayout;\n  width: number;\n  height: number;\n  padding: SparklinePadding;\n  curve: SparklineCurve;\n  strokeWidth: number;\n  color: string;\n  fill: \"gradient\" | \"solid\";\n  fillOpacity: number;\n  endDot: boolean;\n  hoverIndex: number | null;\n  crosshair: SparklineCrosshair;\n  dot: boolean;\n  gradientId: string;\n  animate: boolean;\n  animationDuration: number;\n  animationEasing: string;\n  dataTransitionDuration: number;\n  groupRef: RefObject<SVGGElement | null>;\n  lineRef: RefObject<SVGPathElement | null>;\n  spring: SparklineSpringConfig | null;\n  showHighlight: boolean;\n  inactiveOpacity: number;\n}) {\n  const linePath = useMemo(\n    () => buildSparklineLinePath(layout.points, curve),\n    [layout, curve]\n  );\n  const areaPath = useMemo(\n    () =>\n      variant === \"area\"\n        ? buildSparklineAreaPath(layout.points, curve, layout.baselineY)\n        : \"\",\n    [variant, layout, curve]\n  );\n\n  const areaRef = useRef<SVGPathElement>(null);\n  useDataTransition(layout, width, dataTransitionDuration, (frame) => {\n    lineRef.current?.setAttribute(\n      \"d\",\n      buildSparklineLinePath(frame.points, curve)\n    );\n    areaRef.current?.setAttribute(\n      \"d\",\n      buildSparklineAreaPath(frame.points, curve, frame.baselineY)\n    );\n  });\n\n  // Line draws in along its stroke; area wipes in left → right.\n  useEnterAnimation(variant === \"line\" ? lineRef : groupRef, width > 0, {\n    animate,\n    duration: animationDuration,\n    easing: animationEasing,\n    keyframes: variant === \"line\" ? drawKeyframes : wipeKeyframes,\n  });\n\n  const fillId = `${gradientId}-fill`;\n  let last: SparklineLayout[\"points\"][number] | undefined;\n  for (let i = layout.points.length - 1; i >= 0 && !last; i--) {\n    const point = layout.points[i];\n    if (point && Number.isFinite(point.y)) {\n      last = point;\n    }\n  }\n\n  return (\n    <>\n      <g\n        aria-hidden\n        ref={groupRef}\n        style={{\n          opacity: hoverIndex === null ? 1 : inactiveOpacity,\n          transition: HOVER_FADE,\n        }}\n      >\n        {variant === \"area\" ? (\n          <>\n            {fill === \"gradient\" ? (\n              <defs>\n                <linearGradient id={fillId} x1=\"0\" x2=\"0\" y1=\"0\" y2=\"1\">\n                  <stop\n                    offset=\"0\"\n                    stopColor={color}\n                    stopOpacity={fillOpacity}\n                  />\n                  <stop offset=\"1\" stopColor={color} stopOpacity={0} />\n                </linearGradient>\n              </defs>\n            ) : null}\n            <path\n              d={areaPath}\n              fill={fill === \"gradient\" ? `url(#${fillId})` : color}\n              fillOpacity={fill === \"gradient\" ? 1 : fillOpacity}\n              ref={areaRef}\n            />\n          </>\n        ) : null}\n        <path\n          d={linePath}\n          fill=\"none\"\n          pathLength={1}\n          ref={lineRef}\n          stroke={color}\n          strokeDasharray={variant === \"line\" ? 1 : undefined}\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          strokeWidth={strokeWidth}\n        />\n      </g>\n      {showHighlight && hoverIndex !== null ? (\n        <SparklineHighlight\n          height={height}\n          hoverIndex={hoverIndex}\n          layout={layout}\n          lineRef={lineRef}\n          stroke={color}\n          strokeWidth={strokeWidth}\n        />\n      ) : null}\n      {endDot && hoverIndex === null && last ? (\n        <SparklineDot color={color} x={last.x} y={last.y} />\n      ) : null}\n      <HoverOverlay\n        color={color}\n        crosshair={crosshair}\n        dot={dot}\n        gradientId={gradientId}\n        height={height}\n        hoverIndex={hoverIndex}\n        layout={layout}\n        padding={padding}\n        spring={spring}\n      />\n    </>\n  );\n}\n\nfunction LineSparklineImpl<T>(props: LineSparklineProps<T>) {\n  return <LineOrArea config={props} variant=\"line\" />;\n}\n\nfunction AreaSparklineImpl<T>(props: AreaSparklineProps<T>) {\n  return <LineOrArea config={props} variant=\"area\" />;\n}\n\nfunction BarSparklineImpl<T>(props: BarSparklineProps<T>) {\n  const {\n    barGap = 2,\n    barRadius = 1,\n    inactiveOpacity = 0.4,\n    animate = true,\n    animationDuration = DEFAULT_SPARKLINE_ANIMATION_DURATION_MS,\n    animationEasing = DEFAULT_ANIMATION_EASING,\n    dataTransitionDuration = DEFAULT_SPARKLINE_DATA_TRANSITION_MS,\n    crosshair = \"dashed\",\n    dot = true,\n    hoverTransition = true,\n    springConfig,\n  } = props;\n  const spring = resolveHoverSpring(hoverTransition, springConfig);\n  const padding = resolveSparklinePadding(props.padding, 2);\n  const groupRef = useRef<SVGGElement>(null);\n\n  return (\n    <SparklineFrame config={props} defaultPadding={2} includeZero kind=\"band\">\n      {({ width, height, layout, hoverIndex, color, gradientId }) => (\n        <BarPaths\n          animate={animate}\n          animationDuration={animationDuration}\n          animationEasing={animationEasing}\n          barGap={barGap}\n          barRadius={barRadius}\n          color={color}\n          crosshair={crosshair}\n          dataTransitionDuration={animate ? dataTransitionDuration : 0}\n          dot={dot}\n          gradientId={gradientId}\n          groupRef={groupRef}\n          height={height}\n          hoverIndex={hoverIndex}\n          inactiveOpacity={inactiveOpacity}\n          layout={layout}\n          padding={padding}\n          spring={spring}\n          width={width}\n        />\n      )}\n    </SparklineFrame>\n  );\n}\n\nfunction BarPaths({\n  layout,\n  width,\n  height,\n  padding,\n  barGap,\n  barRadius,\n  inactiveOpacity,\n  color,\n  hoverIndex,\n  crosshair,\n  dot,\n  gradientId,\n  animate,\n  animationDuration,\n  animationEasing,\n  dataTransitionDuration,\n  groupRef,\n  spring,\n}: {\n  layout: SparklineLayout;\n  width: number;\n  height: number;\n  padding: SparklinePadding;\n  barGap: number;\n  barRadius: number;\n  inactiveOpacity: number;\n  color: string;\n  hoverIndex: number | null;\n  crosshair: SparklineCrosshair;\n  dot: boolean;\n  gradientId: string;\n  animate: boolean;\n  animationDuration: number;\n  animationEasing: string;\n  dataTransitionDuration: number;\n  groupRef: RefObject<SVGGElement | null>;\n  spring: SparklineSpringConfig | null;\n}) {\n  const barsRef = useRef<SVGPathElement>(null);\n  useDataTransition(layout, width, dataTransitionDuration, (frame) => {\n    barsRef.current?.setAttribute(\n      \"d\",\n      buildSparklineBarsPath(frame, { gap: barGap, radius: barRadius })\n    );\n  });\n  const barsPath = useMemo(\n    () => buildSparklineBarsPath(layout, { gap: barGap, radius: barRadius }),\n    [layout, barGap, barRadius]\n  );\n\n  // Bars grow from the baseline while a left → right wipe staggers them.\n  const keyframes = useMemo<Keyframe[]>(\n    () => [\n      {\n        clipPath: \"inset(0 100% 0 0)\",\n        transform: \"scaleY(0)\",\n        transformOrigin: `0 ${layout.baselineY}px`,\n      },\n      {\n        clipPath: \"inset(0 0% 0 0)\",\n        transform: \"scaleY(1)\",\n        transformOrigin: `0 ${layout.baselineY}px`,\n      },\n    ],\n    [layout.baselineY]\n  );\n  useEnterAnimation(groupRef, width > 0, {\n    animate,\n    duration: animationDuration,\n    easing: animationEasing,\n    keyframes,\n  });\n\n  const hovered = hoverIndex === null ? undefined : layout.points[hoverIndex];\n\n  return (\n    <>\n      <g aria-hidden ref={groupRef}>\n        <path\n          d={barsPath}\n          fill={color}\n          ref={barsRef}\n          style={{\n            opacity: hovered ? inactiveOpacity : 1,\n            transition: HOVER_FADE,\n          }}\n        />\n        {/* The focused bar sits on top at full strength; moving between bars\n            cross-fades it, and the rest fade with the base path above. */}\n        <AnimatePresence>\n          {hovered && hoverIndex !== null && Number.isFinite(hovered.y) ? (\n            <motion.path\n              animate={{ opacity: 1 }}\n              d={buildSparklineBarSegment(\n                hovered.x,\n                sparklineBarWidth(layout.band, barGap),\n                hovered.y,\n                layout.baselineY,\n                barRadius\n              )}\n              exit={{ opacity: 0 }}\n              fill={color}\n              initial={{ opacity: inactiveOpacity }}\n              key={hoverIndex}\n              transition={{ duration: 0.2, ease: \"easeInOut\" }}\n            />\n          ) : null}\n        </AnimatePresence>\n      </g>\n      <HoverOverlay\n        color={color}\n        crosshair={crosshair}\n        dot={dot}\n        gradientId={gradientId}\n        height={height}\n        hoverIndex={hoverIndex}\n        layout={layout}\n        padding={padding}\n        spring={spring}\n      />\n    </>\n  );\n}\n\n/** Tiny line chart — one path, no axes, minimal hover crosshair + dot. */\nexport const LineSparkline = memo(\n  LineSparklineImpl\n) as typeof LineSparklineImpl;\n/** Tiny area chart — line over a gradient or solid fill. */\nexport const AreaSparkline = memo(\n  AreaSparklineImpl\n) as typeof AreaSparklineImpl;\n/** Tiny bar chart — all bars drawn as a single path. */\nexport const BarSparkline = memo(BarSparklineImpl) as typeof BarSparklineImpl;\n\n/**\n * Lift a sparkline's hover state so you can display the hovered value.\n * Spread `sparklineProps` onto one sparkline — or several to sync their\n * crosshairs.\n *\n * ```tsx\n * const hover = useSparklineHover<Row>();\n * <span>{hover.datum?.value ?? latest}</span>\n * <LineSparkline data={rows} dataKey=\"value\" {...hover.sparklineProps} />\n * ```\n */\nexport function useSparklineHover<T>() {\n  const [state, setState] = useState<{ index: number | null; datum: T | null }>(\n    { index: null, datum: null }\n  );\n  const onHoverChange = useCallback(\n    (index: number | null, datum: T | null) => setState({ index, datum }),\n    []\n  );\n  const sparklineProps = useMemo(\n    () => ({ hoverIndex: state.index, onHoverChange }),\n    [state.index, onHoverChange]\n  );\n  return {\n    index: state.index,\n    datum: state.datum,\n    isHovering: state.index !== null,\n    sparklineProps,\n  };\n}\n",
      "type": "registry:component",
      "target": "components/charts/sparkline.tsx"
    },
    {
      "path": "src/charts/sparkline-geometry.ts",
      "content": "import {\n  type CurveFactory,\n  curveLinear,\n  curveMonotoneX,\n  curveNatural,\n  curveStep,\n  area as d3Area,\n  line as d3Line,\n} from \"d3-shape\";\n\nexport type SparklineCurve = \"linear\" | \"monotone\" | \"natural\" | \"step\";\n\nexport type SparklineDataKey<T> =\n  | keyof T\n  | ((datum: T, index: number) => number);\n\nexport interface SparklinePadding {\n  top: number;\n  right: number;\n  bottom: number;\n  left: number;\n}\n\nexport interface SparklinePoint {\n  x: number;\n  y: number;\n  /** NaN when the source value is missing. */\n  value: number;\n}\n\nexport interface SparklineLayout {\n  points: SparklinePoint[];\n  /** Pixel y of the baseline (0, clamped into the domain). */\n  baselineY: number;\n  /** Pixel width of one bar slot (bars only; 0 for line/area). */\n  band: number;\n  domain: [number, number];\n}\n\nconst curves: Record<SparklineCurve, CurveFactory> = {\n  linear: curveLinear,\n  monotone: curveMonotoneX,\n  natural: curveNatural,\n  step: curveStep,\n};\n\nexport function resolveSparklinePadding(\n  padding: number | Partial<SparklinePadding> | undefined,\n  fallback: number\n): SparklinePadding {\n  if (typeof padding === \"number\" || padding === undefined) {\n    const p = padding ?? fallback;\n    return { top: p, right: p, bottom: p, left: p };\n  }\n  return {\n    top: padding.top ?? fallback,\n    right: padding.right ?? fallback,\n    bottom: padding.bottom ?? fallback,\n    left: padding.left ?? fallback,\n  };\n}\n\nexport function resolveSparklineValues<T>(\n  data: readonly T[],\n  dataKey: SparklineDataKey<T>\n): number[] {\n  return data.map((datum, index) => {\n    const raw =\n      typeof dataKey === \"function\"\n        ? dataKey(datum, index)\n        : (datum as Record<PropertyKey, unknown>)[dataKey as PropertyKey];\n    const value = typeof raw === \"number\" ? raw : Number(raw);\n    return raw === null || raw === undefined ? Number.NaN : value;\n  });\n}\n\n/**\n * Value domain for the sparkline. Bars always include zero so heights read\n * honestly; line/area hug the data unless `includeZero` is set.\n */\nexport function computeSparklineDomain(\n  values: readonly number[],\n  options: { yDomain?: [number?, number?]; includeZero?: boolean } = {}\n): [number, number] {\n  let min = Number.POSITIVE_INFINITY;\n  let max = Number.NEGATIVE_INFINITY;\n  for (const value of values) {\n    if (Number.isFinite(value)) {\n      if (value < min) {\n        min = value;\n      }\n      if (value > max) {\n        max = value;\n      }\n    }\n  }\n  if (min === Number.POSITIVE_INFINITY) {\n    min = 0;\n    max = 0;\n  }\n  if (options.includeZero) {\n    min = Math.min(min, 0);\n    max = Math.max(max, 0);\n  }\n  min = options.yDomain?.[0] ?? min;\n  max = options.yDomain?.[1] ?? max;\n  if (min === max) {\n    // Flat series: centre the line instead of pinning it to an edge.\n    return [min - 1, max + 1];\n  }\n  return [min, max];\n}\n\nexport function computeSparklineLayout(\n  values: readonly number[],\n  options: {\n    width: number;\n    height: number;\n    padding: SparklinePadding;\n    domain: [number, number];\n    kind: \"point\" | \"band\";\n  }\n): SparklineLayout {\n  const { width, height, padding, domain, kind } = options;\n  const innerWidth = Math.max(0, width - padding.left - padding.right);\n  const innerHeight = Math.max(0, height - padding.top - padding.bottom);\n  const [d0, d1] = domain;\n  const yOf = (value: number) =>\n    padding.top + innerHeight - ((value - d0) / (d1 - d0)) * innerHeight;\n\n  const n = values.length;\n  const band = kind === \"band\" && n > 0 ? innerWidth / n : 0;\n  const step = n > 1 ? innerWidth / (n - 1) : 0;\n\n  const points = values.map((value, i) => ({\n    x:\n      kind === \"band\"\n        ? padding.left + band * (i + 0.5)\n        : padding.left + (n > 1 ? step * i : innerWidth / 2),\n    y: Number.isFinite(value) ? yOf(value) : Number.NaN,\n    value,\n  }));\n\n  const baseline = Math.min(Math.max(0, d0), d1);\n  return { points, baselineY: yOf(baseline), band, domain };\n}\n\nconst isDefined = (point: SparklinePoint) => Number.isFinite(point.y);\n\nexport function buildSparklineLinePath(\n  points: readonly SparklinePoint[],\n  curve: SparklineCurve\n): string {\n  return (\n    d3Line<SparklinePoint>()\n      .defined(isDefined)\n      .x((p) => p.x)\n      .y((p) => p.y)\n      .curve(curves[curve])(points as SparklinePoint[]) ?? \"\"\n  );\n}\n\nexport function buildSparklineAreaPath(\n  points: readonly SparklinePoint[],\n  curve: SparklineCurve,\n  baselineY: number\n): string {\n  return (\n    d3Area<SparklinePoint>()\n      .defined(isDefined)\n      .x((p) => p.x)\n      .y0(baselineY)\n      .y1((p) => p.y)\n      .curve(curves[curve])(points as SparklinePoint[]) ?? \"\"\n  );\n}\n\nconst round = (value: number) => Math.round(value * 100) / 100;\n\n/** One bar as a path segment, rounding the corners away from the baseline. */\nexport function buildSparklineBarSegment(\n  cx: number,\n  barWidth: number,\n  y: number,\n  baselineY: number,\n  radius: number\n): string {\n  const height = Math.abs(baselineY - y);\n  if (height === 0 || barWidth <= 0) {\n    return \"\";\n  }\n  const x0 = round(cx - barWidth / 2);\n  const x1 = round(cx + barWidth / 2);\n  const r = Math.min(radius, barWidth / 2, height);\n  const up = y < baselineY;\n  const tip = round(y);\n  const base = round(baselineY);\n  if (r <= 0) {\n    return `M${x0},${base}V${tip}H${x1}V${base}Z`;\n  }\n  const dir = up ? 1 : -1;\n  const rr = round(r);\n  const sweep = up ? 1 : 0;\n  return (\n    `M${x0},${base}V${round(tip + dir * r)}` +\n    `A${rr},${rr} 0 0 ${sweep} ${round(x0 + r)},${tip}` +\n    `H${round(x1 - r)}` +\n    `A${rr},${rr} 0 0 ${sweep} ${x1},${round(tip + dir * r)}` +\n    `V${base}Z`\n  );\n}\n\nexport function sparklineBarWidth(band: number, gap: number): number {\n  return Math.max(1, band - gap);\n}\n\n/** All bars as a single path — one DOM node regardless of point count. */\nexport function buildSparklineBarsPath(\n  layout: SparklineLayout,\n  options: { gap: number; radius: number }\n): string {\n  const barWidth = sparklineBarWidth(layout.band, options.gap);\n  let d = \"\";\n  for (const point of layout.points) {\n    if (Number.isFinite(point.y)) {\n      d += buildSparklineBarSegment(\n        point.x,\n        barWidth,\n        point.y,\n        layout.baselineY,\n        options.radius\n      );\n    }\n  }\n  return d;\n}\n\n/** Nearest data index for a pointer x position (in chart pixels). */\nexport function sparklineIndexAtX(\n  x: number,\n  options: {\n    count: number;\n    width: number;\n    padding: SparklinePadding;\n    kind: \"point\" | \"band\";\n  }\n): number | null {\n  const { count, width, padding, kind } = options;\n  if (count === 0) {\n    return null;\n  }\n  const innerWidth = width - padding.left - padding.right;\n  if (innerWidth <= 0) {\n    return 0;\n  }\n  const t = (x - padding.left) / innerWidth;\n  const raw =\n    kind === \"band\" ? Math.floor(t * count) : Math.round(t * (count - 1));\n  return Math.min(count - 1, Math.max(0, raw));\n}\n\nfunction formatCompact(value: number): string {\n  return new Intl.NumberFormat(\"en\", {\n    notation: \"compact\",\n    maximumFractionDigits: 1,\n  }).format(value);\n}\n\n/** Screen-reader summary, e.g. \"12 points, from 3.1K to 4.2K, up 35%\". */\nexport function describeSparkline(values: readonly number[]): string {\n  const finite = values.filter(Number.isFinite);\n  if (finite.length === 0) {\n    return \"No data\";\n  }\n  const first = finite[0] as number;\n  const last = finite.at(-1) as number;\n  let summary = `${finite.length} points, from ${formatCompact(first)} to ${formatCompact(last)}`;\n  if (first !== 0) {\n    const change = ((last - first) / Math.abs(first)) * 100;\n    const direction = change >= 0 ? \"up\" : \"down\";\n    summary += `, ${direction} ${Math.abs(Math.round(change))}%`;\n  }\n  return summary;\n}\n\n/**\n * Tween frame between two layouts. Points take the target x; the previous\n * shape is resampled onto the target grid so series of different lengths\n * rise and fall into place. Missing values snap.\n */\nexport function interpolateSparklineLayout(\n  from: SparklineLayout,\n  to: SparklineLayout,\n  t: number\n): SparklineLayout {\n  const m = from.points.length;\n  const n = to.points.length;\n  const fromYAt = (i: number): number => {\n    if (m === 0) {\n      return Number.NaN;\n    }\n    if (m === n) {\n      return from.points[i]?.y ?? Number.NaN;\n    }\n    const position = n > 1 ? (i / (n - 1)) * (m - 1) : 0;\n    const lo = Math.floor(position);\n    const a = from.points[lo]?.y ?? Number.NaN;\n    const b = from.points[Math.min(m - 1, lo + 1)]?.y ?? Number.NaN;\n    return a + (b - a) * (position - lo);\n  };\n  return {\n    ...to,\n    baselineY: from.baselineY + (to.baselineY - from.baselineY) * t,\n    points: to.points.map((point, i) => {\n      const fromY = fromYAt(i);\n      return Number.isFinite(fromY) && Number.isFinite(point.y)\n        ? { ...point, y: fromY + (point.y - fromY) * t }\n        : point;\n    }),\n  };\n}\n",
      "type": "registry:component",
      "target": "components/charts/sparkline-geometry.ts"
    }
  ]
}