{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sparkline",
  "type": "registry:ui",
  "title": "Sparkline",
  "description": "Compact line, area and bar trends colored by the --dt-color-chart palette, with min/max/last markers, an accessible summary, and reduced-motion-safe draw-in.",
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [
    "https://components.dethink.co.uk/r/dethink-base.json",
    "https://components.dethink.co.uk/r/chart-core.json"
  ],
  "files": [
    {
      "path": "packages/components/src/components/sparkline/sparkline.tsx",
      "type": "registry:file",
      "target": "~/components/dethink/components/sparkline/sparkline.tsx",
      "content": "import { forwardRef, useId, type HTMLAttributes } from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport {\n  areaPath,\n  barPath,\n  definedSegments,\n  extent,\n  formatChartValue,\n  isFiniteValue,\n  linePath,\n  resolveChartColor,\n  scaleBand,\n  scaleLinear,\n  summarizeSeries,\n  type ChartColor,\n  type ChartCurve,\n  type ChartValue,\n} from \"../chart/chart-core\";\nimport { useChartSize } from \"../chart/use-chart-size\";\n\nexport type SparklineVariant = \"line\" | \"area\" | \"bar\";\nexport type SparklineMarker = \"first\" | \"last\" | \"min\" | \"max\";\nexport type SparklineEmphasis = \"last\" | \"none\";\n\nexport interface SparklineProps extends Omit<\n  HTMLAttributes<HTMLDivElement>,\n  \"children\" | \"color\"\n> {\n  /** Values in order. `null`/`undefined` leave a gap. */\n  data: readonly ChartValue[];\n  variant?: SparklineVariant;\n  /** Palette slot (`\"chart-3\"`) or any CSS color. */\n  color?: ChartColor;\n  curve?: ChartCurve;\n  /** Point markers for line and area variants. `true` shows min, max and last. */\n  markers?: boolean | readonly SparklineMarker[];\n  /** Bar variant: mute every bar except the latest period. */\n  emphasis?: SparklineEmphasis;\n  /** Fixed value domain. Defaults to the data extent (bars always include zero). */\n  domain?: readonly [number, number];\n  /** Start of the accessible summary, for example \"Revenue, last 12 weeks\". */\n  label?: string;\n  formatValue?: (value: number) => string;\n  /** Hide from assistive technology when surrounding text already carries the data. */\n  decorative?: boolean;\n  /** Draw-in on mount. Always off under reduced motion. */\n  animate?: boolean;\n}\n\n// Fallback geometry used before the first client measurement (and on the server).\nconst FALLBACK_WIDTH = 120;\nconst FALLBACK_HEIGHT = 32;\nconst PADDING = 4;\n\nconst sparklineRootClasses =\n  \"relative inline-block h-8 w-full min-w-16 align-middle text-[var(--sparkline-color)] [--sparkline-surface:var(--dt-chart-surface,var(--dt-color-background))]\";\n\nconst drawClasses =\n  \"motion-safe:[stroke-dasharray:1] motion-safe:animate-[dt-chart-draw_900ms_var(--ease-control)_both]\";\nconst fadeClasses =\n  \"motion-safe:animate-[dt-chart-fade_600ms_ease-out_150ms_both]\";\nconst markerClasses =\n  \"motion-safe:animate-[dt-chart-pop_320ms_var(--ease-control)_750ms_both] [transform-box:fill-box] [transform-origin:center]\";\nconst growClasses =\n  \"motion-safe:animate-[dt-chart-grow_600ms_var(--ease-control)_both] [transform-box:fill-box] [transform-origin:bottom]\";\n\nexport function describeSparkline(\n  data: readonly ChartValue[],\n  label = \"Trend\",\n  formatValue: (value: number) => string = (value) => formatChartValue(value),\n) {\n  const summary = summarizeSeries(data);\n  if (summary.count === 0) return `${label}: no data`;\n  if (summary.count === 1) return `${label}: ${formatValue(summary.last!)}`;\n\n  return (\n    `${label}: ${summary.count} values, from ${formatValue(summary.first!)} ` +\n    `to ${formatValue(summary.last!)}; low ${formatValue(summary.min!)}, ` +\n    `high ${formatValue(summary.max!)}`\n  );\n}\n\nfunction lastDefinedIndex(data: readonly ChartValue[]) {\n  for (let index = data.length - 1; index >= 0; index -= 1) {\n    if (isFiniteValue(data[index])) return index;\n  }\n  return -1;\n}\n\nfunction resolveMarkers(\n  markers: SparklineProps[\"markers\"],\n): readonly SparklineMarker[] {\n  if (markers === true) return [\"min\", \"max\", \"last\"];\n  if (markers === false) return [];\n  return markers ?? [\"last\"];\n}\n\nexport const Sparkline = forwardRef<HTMLDivElement, SparklineProps>(\n  (\n    {\n      animate = true,\n      className,\n      color = \"chart-1\",\n      curve = \"monotone\",\n      data,\n      decorative = false,\n      domain,\n      emphasis = \"last\",\n      formatValue,\n      label,\n      markers,\n      style,\n      variant = \"line\",\n      ...props\n    },\n    forwardedRef,\n  ) => {\n    const gradientId = useId();\n    const [measureRef, size] = useChartSize<HTMLDivElement>();\n    const measured = size !== undefined && size.width > 0 && size.height > 0;\n    const width = measured ? size.width : FALLBACK_WIDTH;\n    const height = measured ? size.height : FALLBACK_HEIGHT;\n\n    const bounds = extent(data) ?? [0, 1];\n    const valueBounds: readonly [number, number] =\n      domain ??\n      (variant === \"bar\"\n        ? [Math.min(0, bounds[0]), Math.max(0, bounds[1])]\n        : bounds);\n    const y = scaleLinear(valueBounds, [height - PADDING, PADDING]);\n    const lastIndex = data.length - 1;\n    const x = scaleLinear(\n      [0, Math.max(1, lastIndex)],\n      [PADDING, width - PADDING],\n    );\n    const segments = definedSegments(data, (value, index) => ({\n      x: data.length === 1 ? width / 2 : x(index),\n      y: y(value),\n    }));\n\n    const summary = summarizeSeries(data);\n    const markerIndexes = new Set<number>();\n    if (variant !== \"bar\" && measured) {\n      for (const marker of resolveMarkers(markers)) {\n        const index =\n          marker === \"min\"\n            ? summary.minIndex\n            : marker === \"max\"\n              ? summary.maxIndex\n              : marker === \"first\"\n                ? data.findIndex(isFiniteValue)\n                : lastDefinedIndex(data);\n        if (index !== undefined && index >= 0) markerIndexes.add(index);\n      }\n    }\n\n    const setRef = (node: HTMLDivElement | null) => {\n      measureRef.current = node;\n      if (typeof forwardedRef === \"function\") forwardedRef(node);\n      else if (forwardedRef) forwardedRef.current = node;\n    };\n\n    const motion = animate ? undefined : \"none\";\n    const stroke = measured ? undefined : \"non-scaling-stroke\";\n\n    return (\n      <div\n        {...props}\n        ref={setRef}\n        role={decorative ? undefined : \"img\"}\n        aria-hidden={decorative ? true : undefined}\n        aria-label={\n          decorative ? undefined : describeSparkline(data, label, formatValue)\n        }\n        data-slot=\"sparkline\"\n        data-variant={variant}\n        className={cn(sparklineRootClasses, className)}\n        style={{\n          ...style,\n          [\"--sparkline-color\" as string]: resolveChartColor(color),\n        }}\n      >\n        <svg\n          aria-hidden=\"true\"\n          focusable=\"false\"\n          className=\"absolute inset-0 size-full overflow-visible\"\n          viewBox={`0 0 ${width} ${height}`}\n          preserveAspectRatio={measured ? undefined : \"none\"}\n        >\n          {variant === \"area\" ? (\n            <defs>\n              <linearGradient id={gradientId} x1=\"0\" x2=\"0\" y1=\"0\" y2=\"1\">\n                <stop offset=\"0%\" stopColor=\"currentColor\" stopOpacity={0.28} />\n                <stop offset=\"100%\" stopColor=\"currentColor\" stopOpacity={0} />\n              </linearGradient>\n            </defs>\n          ) : null}\n\n          {variant === \"bar\"\n            ? renderBars()\n            : segments.map((points, i) =>\n                points.length === 1 ? (\n                  // A value isolated between gaps has no line; draw it as a dot.\n                  // A zero-length round-capped stroke stays round when stretched.\n                  <path\n                    key={i}\n                    data-slot=\"sparkline-point\"\n                    d={`M${points[0]!.x},${points[0]!.y}h0`}\n                    fill=\"none\"\n                    stroke=\"currentColor\"\n                    strokeWidth={4}\n                    strokeLinecap=\"round\"\n                    vectorEffect={stroke}\n                    className={fadeClasses}\n                    style={{ animation: motion }}\n                  />\n                ) : (\n                  <g key={i}>\n                    {variant === \"area\" ? (\n                      <path\n                        data-slot=\"sparkline-area\"\n                        d={areaPath(points, height, curve)}\n                        fill={`url(#${gradientId})`}\n                        className={fadeClasses}\n                        style={{ animation: motion }}\n                      />\n                    ) : null}\n                    <path\n                      data-slot=\"sparkline-line\"\n                      d={linePath(points, curve)}\n                      pathLength={1}\n                      fill=\"none\"\n                      stroke=\"currentColor\"\n                      strokeWidth={2}\n                      strokeLinecap=\"round\"\n                      strokeLinejoin=\"round\"\n                      vectorEffect={stroke}\n                      className={drawClasses}\n                      style={{ animation: motion, strokeDasharray: motion }}\n                    />\n                  </g>\n                ),\n              )}\n\n          {[...markerIndexes].map((index) => (\n            <circle\n              key={index}\n              data-slot=\"sparkline-marker\"\n              cx={data.length === 1 ? width / 2 : x(index)}\n              cy={y(data[index] as number)}\n              r={3}\n              fill=\"currentColor\"\n              stroke=\"var(--sparkline-surface)\"\n              strokeWidth={2}\n              paintOrder=\"stroke\"\n              className={markerClasses}\n              style={{ animation: motion }}\n            />\n          ))}\n        </svg>\n      </div>\n    );\n\n    function renderBars() {\n      const band = scaleBand(\n        data.map((_, index) => String(index)),\n        [PADDING / 2, width - PADDING / 2],\n        { paddingInner: 0.28, paddingOuter: 0 },\n      );\n      const barWidth = Math.min(band.bandwidth, 24);\n      const offset = (band.bandwidth - barWidth) / 2;\n      const baseline = y(Math.max(valueBounds[0], Math.min(0, valueBounds[1])));\n\n      return data.map((value, index) => {\n        if (!isFiniteValue(value)) return null;\n        const d = barPath({\n          x: band(String(index))! + offset,\n          width: barWidth,\n          baseline,\n          value: y(value),\n          radius: Math.min(2, barWidth / 2),\n        });\n        if (!d) return null;\n        const muted = emphasis === \"last\" && index !== lastIndex;\n        return (\n          <path\n            key={index}\n            data-slot=\"sparkline-bar\"\n            data-emphasis={muted ? \"muted\" : undefined}\n            d={d}\n            fill=\"currentColor\"\n            fillOpacity={muted ? 0.38 : 1}\n            className={growClasses}\n            style={{\n              animation: motion,\n              animationDelay: animate ? `${index * 24}ms` : undefined,\n            }}\n          />\n        );\n      });\n    }\n  },\n);\n\nSparkline.displayName = \"Sparkline\";\n"
    },
    {
      "path": "packages/components/src/components/sparkline/index.ts",
      "type": "registry:file",
      "target": "~/components/dethink/components/sparkline/index.ts",
      "content": "export {\n  Sparkline,\n  describeSparkline,\n  type SparklineEmphasis,\n  type SparklineMarker,\n  type SparklineProps,\n  type SparklineVariant,\n} from \"./sparkline\";\n"
    },
    {
      "path": "packages/components/src/utils/cn.ts",
      "type": "registry:file",
      "target": "~/components/dethink/utils/cn.ts",
      "content": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"
    }
  ]
}
