{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sparkline",
  "title": "Sparkline",
  "description": "A 44px trend line with no axes, a dot on the current point, a hover value and the same tokens as TimeseriesChart.",
  "dependencies": [
    "@tanstack/charts@^1.2.1"
  ],
  "registryDependencies": [
    "https://sproutboat.com/ikada/r/theme.json",
    "https://sproutboat.com/ikada/r/accessible-name.json",
    "https://sproutboat.com/ikada/r/chart-theme.json",
    "https://sproutboat.com/ikada/r/cn.json",
    "https://sproutboat.com/ikada/r/format.json",
    "https://sproutboat.com/ikada/r/tooltip-card.json"
  ],
  "files": [
    {
      "path": "registry/ikada/ui/sparkline.tsx",
      "content": "\"use client\";\n\nimport { areaY, defineChart, dot, lineY } from \"@tanstack/charts\";\nimport { decorative } from \"@tanstack/charts/mark/decorative\";\nimport { Chart } from \"@tanstack/charts/react\";\nimport { scaleLinear } from \"@tanstack/charts/scales/linear\";\nimport { tooltip } from \"@tanstack/charts/tooltip\";\nimport { portal } from \"@tanstack/charts/tooltip/portal\";\nimport { useId, useMemo } from \"react\";\nimport { cn } from \"@/components/ikada/lib/cn\";\nimport { formatNumber } from \"@/components/ikada/lib/format\";\nimport { accessibleName } from \"@/components/ikada/lib/accessible-name\";\nimport type { AccessibleName } from \"@/components/ikada/lib/accessible-name\";\nimport { tooltipCard } from \"@/components/ikada/tooltip-card\";\nimport { chartClassName, chartColors } from \"@/components/ikada/chart-theme\";\nimport type { ChartColorName } from \"@/components/ikada/chart-theme\";\n\nexport interface SparklineBase {\n  /** Values in time order, oldest first. */\n  data: readonly number[];\n  /** Chart colour token for the line. Default `requests`. */\n  color?: ChartColorName;\n  /** Height in pixels. Default 44; the width fills the container. */\n  height?: number;\n  /** Fill under the line. Defaults to true. */\n  area?: boolean;\n  /** [min, max]. Defaults to the data's own range with a little air above and below, so a quiet series still shows its shape; pass [0, max] to compare several on one honest scale. */\n  domain?: readonly [number, number];\n  /** What the number is, shown once next to the value in the hover tooltip (\"Requests  10,303\"), so `formatValue` should not repeat it. Defaults to the first words of `ariaLabel` (\"Requests\" from \"Requests, last 24 hours\"). */\n  label?: string;\n  /** Formats the hover value. */\n  formatValue?: (value: number) => string;\n  /** Title for a hovered point, usually its time (\"14:00\"). Without it the card shows just the row (\"Requests  11\"): a position like \"Point 3\" says nothing. */\n  formatPoint?: (index: number) => string;\n  /** A dot on the newest value, so the eye lands on \"now\". Defaults to true. */\n  current?: boolean;\n  className?: string;\n}\n\n/** Room kept around a point for the hover ring (radius 4 plus half its 2px stroke = 5px, plus 1px): above the lowest value, and at the sides, so the first and last points sit `SIDE` px inside the Sparkline's box. A MetricTile pads its slot by (16 - SIDE) px, so the line's ends line up with the tile's text. */\nconst FLOOR = 7;\nconst SIDE = 6;\nconst MARGIN = { top: 6, right: SIDE, bottom: 0, left: SIDE } as const;\n\nconst defaultValue = (value: number): string => formatNumber(value);\n\ninterface Row {\n  i: number;\n  v: number;\n}\n\n/** Props of {@link Sparkline}. The accessible name is `aria-label` (e.g. \"Requests, last 24 hours\"); `ariaLabel` is deprecated. */\nexport type SparklineProps = SparklineBase & AccessibleName;\n\n/** A 44px trend line with no axes, a dot on the current point, a hover value and the same tokens as TimeseriesChart. */\nexport function Sparkline({\n  data: rawData,\n  \"aria-label\": ariaLabelStd,\n  ariaLabel: ariaLabelLegacy,\n  color = \"requests\",\n  height = 44,\n  area = true,\n  domain,\n  label,\n  formatValue = defaultValue,\n  formatPoint,\n  current = true,\n  className,\n}: SparklineProps) {\n  const gradientId = `sb-spark-${useId().replaceAll(/[^a-zA-Z0-9_-]/g, \"\")}`;\n  const ariaLabel = accessibleName({ \"aria-label\": ariaLabelStd, ariaLabel: ariaLabelLegacy });\n  // A NaN or Infinity (a failed division upstream) would make the scale throw and take the page with it: drop it.\n  const data = useMemo(() => rawData.filter(Number.isFinite), [rawData]);\n  const definition = useMemo(() => {\n    const rows: Row[] = data.map((v, i) => ({ i, v }));\n    const last = rows.at(-1);\n    const stroke = chartColors[color];\n    const name = label ?? ariaLabel.split(\",\")[0]!.trim();\n    const lo = data.length ? Math.min(...data) : 0;\n    const hi = data.length ? Math.max(...data) : 1;\n    // Fit the data with air: 14% of the range above (room for the stroke and the dot). Below, the lowest value sits FLOOR px above the tile edge (see below), whatever the height.\n    const span = hi - lo || Math.max(Math.abs(hi), 1) * 0.2;\n    let yMin = domain?.[0] ?? lo;\n    const yMax = domain?.[1] ?? hi + span * 0.14;\n    // The hover ring on the lowest value would be cut by the tile's bottom edge and rounded corners. Lower the scale floor so that value sits FLOOR px up; the fill (y1 = the floor) still runs to the edge. Done in pixels, so it holds at 28px as at 64px, and for an explicit domain too.\n    const plotHeight = Math.max(1, height - MARGIN.top);\n    if (yMax > yMin) yMin = yMax - (yMax - yMin) / (1 - Math.min(FLOOR / plotHeight, 0.5));\n    return defineChart({\n      marks: [\n        ...(area\n          ? [areaY(rows, { x: \"i\", y: \"v\", y1: yMin, fill: `url(#${gradientId})`, fillOpacity: 1, id: \"area\" })]\n          : []),\n        lineY(rows, { x: \"i\", y: \"v\", stroke, strokeWidth: 1.75, id: \"line\" }),\n        // No crosshair: its marker is clipped to the plot rectangle (cutting the ring on the first and last point) and its rule draws through the ring. At 28–64px the focus ring alone marks the point.\n        ...(current && last\n          ? [\n              decorative(\n                dot([last], {\n                  x: \"i\",\n                  y: \"v\",\n                  r: 3,\n                  fill: stroke,\n                  stroke: \"var(--sb-surface)\",\n                  strokeWidth: 1.5,\n                  id: \"now\",\n                }),\n              ),\n            ]\n          : []),\n      ],\n      scales: {\n        x: { scale: scaleLinear().domain([0, Math.max(1, data.length - 1)]), axis: false },\n        y: { scale: scaleLinear().domain([yMin, yMax]), axis: false },\n      },\n      guides: false,\n      // The fill fades out toward the bottom, so it has no hard edge at the foot; at the line ends it stops where the line does. Dropped in forced-colors mode (see theme.css).\n      gradients: [\n        {\n          id: gradientId,\n          x1: 0,\n          y1: 1,\n          x2: 0,\n          y2: 0,\n          stops: [\n            { offset: 0, color: stroke, opacity: 0 },\n            { offset: 1, color: stroke, opacity: 0.14 },\n          ],\n        },\n      ],\n      // Room for the hover ring at the top and sides; the bottom is the tile edge (the floor is handled in the scale).\n      margin: MARGIN,\n      focus: \"nearest-x\",\n      // Drawn after every mark, so it sits over the \"now\" dot instead of beside it; the fill is opaque and wider than the dot.\n      focusRing: { radius: 4, fill: \"var(--sb-surface)\", stroke, strokeWidth: 2 },\n      maxFocusDistance: Number.POSITIVE_INFINITY,\n      tooltip: {\n        use: tooltip,\n        // A floating card in the top layer: never clipped by the tile, flips above or below the point.\n        portal,\n        // Anchored to the hovered point's x and the chart's bottom edge: the card hangs just below the sparkline\n        // (below a MetricTile, never over its label, figure or badge) and only flips above when there is no room.\n        anchor: (points: readonly { x: number }[], context: { surface: { height: number } }) => ({\n          x: points[0]!.x,\n          y: context.surface.height,\n        }),\n        placement: [\"bottom\", \"top\"],\n        offset: 8,\n        content: (points: readonly { datum: unknown }[]) => {\n          const d = points[0]!.datum as Row;\n          return tooltipCard(formatPoint?.(d.i), [\n            { label: name, value: formatValue(d.v), color: stroke, active: true },\n          ]);\n        },\n      },\n    });\n  }, [data, color, area, domain, gradientId, label, ariaLabel, formatValue, formatPoint, current]);\n\n  return (\n    <Chart\n      definition={definition}\n      height={height}\n      ariaLabel={ariaLabel}\n      className={cn(chartClassName, \"w-full\", className)}\n    />\n  );\n}\n",
      "type": "registry:ui",
      "target": "@components/ikada/sparkline.tsx"
    }
  ],
  "docs": "Uses @tanstack/charts (installed with this item). Load it lazily where the chart is below the fold.",
  "categories": [
    "charts"
  ],
  "type": "registry:ui"
}