{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "chart-data",
  "title": "Connecting Real Data",
  "description": "Wiring any chart to a real source: fetch in an effect, SWR or React Query, server components, and streaming — with every outcome mapped onto a status.",
  "files": [
    {
      "path": "app/registry/charts/chart-engine.tsx",
      "content": "'use client';\n\nimport * as React from 'react';\nimport { cn } from '@/lib/utils';\n\nexport const marketVarsClassName =\n  '[--spectrum-chart-up:#059669] [--spectrum-chart-down:#e11d48] [--spectrum-chart-surface:#fff] dark:[--spectrum-chart-up:#34d399] dark:[--spectrum-chart-down:#fb7185] dark:[--spectrum-chart-surface:#0a0a0a]';\n\nexport const UP = 'var(--spectrum-chart-up)';\nexport const DOWN = 'var(--spectrum-chart-down)';\nexport const SURFACE = 'var(--spectrum-chart-surface)';\nexport const EASE = 'cubic-bezier(0.22, 1, 0.36, 1)';\n\nexport type Candle = {\n  t: number;\n  open: number;\n  high: number;\n  low: number;\n  close: number;\n  volume: number;\n};\n\nexport type MarketRange = {\n  label: string;\n  bars: number | null;\n};\n\nexport const MARKET_RANGES: MarketRange[] = [\n  { label: '1W', bars: 7 },\n  { label: '1M', bars: 30 },\n  { label: '3M', bars: 90 },\n  { label: '6M', bars: 180 },\n  { label: '1Y', bars: 365 },\n  { label: 'ALL', bars: null },\n];\n\nexport function mulberry32(seed: number) {\n  let a = seed;\n  return () => {\n    a |= 0;\n    a = (a + 0x6d2b79f5) | 0;\n    let t = Math.imul(a ^ (a >>> 15), 1 | a);\n    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;\n    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n  };\n}\n\nexport const DAY_MS = 86_400_000;\n\nexport function generateCandles({\n  seed,\n  count,\n  start,\n  drift,\n  vol,\n  startedAt,\n}: {\n  seed: number;\n  count: number;\n  start: number;\n  drift: number;\n  vol: number;\n  startedAt: number;\n}): Candle[] {\n  const rand = mulberry32(seed);\n  const out: Candle[] = [];\n  let price = start;\n  let volatility = vol;\n\n  for (let i = 0; i < count; i += 1) {\n    volatility += (vol - volatility) * 0.05 + (rand() - 0.5) * vol * 0.35;\n    volatility = Math.max(vol * 0.35, Math.min(vol * 2.6, volatility));\n\n    const open = price;\n    const shock = (rand() - 0.5) * 2 * volatility + drift;\n    const close = Math.max(0.01, open * (1 + shock));\n    const wick = Math.abs(shock) * 0.9 + volatility * 0.55;\n    const high = Math.max(open, close) * (1 + rand() * wick);\n    const low = Math.min(open, close) * (1 - rand() * wick);\n    const range = Math.abs(close - open) / Math.max(open, 1e-6);\n    const volume = Math.round((0.55 + rand() * 0.7 + range * 26) * 1_000_000);\n\n    out.push({\n      t: startedAt + i * DAY_MS,\n      open: round2(open),\n      high: round2(high),\n      low: round2(low),\n      close: round2(close),\n      volume,\n    });\n    price = close;\n  }\n  return out;\n}\n\nexport function round2(v: number) {\n  return Math.round(v * 100) / 100;\n}\n\nconst SERIES_END = Date.UTC(2026, 7, 21);\nconst SERIES_LEN = 420;\nconst SERIES_START = SERIES_END - (SERIES_LEN - 1) * DAY_MS;\n\nexport const SOL_MARKET = generateCandles({\n  seed: 20_260_823,\n  count: SERIES_LEN,\n  start: 96.4,\n  drift: 0.0021,\n  vol: 0.031,\n  startedAt: SERIES_START,\n});\n\nexport const AAPL_MARKET = generateCandles({\n  seed: 7_431_902,\n  count: SERIES_LEN,\n  start: 189.2,\n  drift: 0.0009,\n  vol: 0.013,\n  startedAt: SERIES_START,\n});\n\nexport const BTC_MARKET = generateCandles({\n  seed: 41_556_073,\n  count: SERIES_LEN,\n  start: 61_400,\n  drift: 0.0016,\n  vol: 0.022,\n  startedAt: SERIES_START,\n});\n\nexport function formatMoney(value: number, compact = false) {\n  const digits = compact ? 2 : value >= 1000 ? 2 : value >= 1 ? 2 : 4;\n  return new Intl.NumberFormat('en-US', {\n    style: 'currency',\n    currency: 'USD',\n    notation: compact ? 'compact' : 'standard',\n    minimumFractionDigits: compact ? 0 : digits,\n    maximumFractionDigits: digits,\n  }).format(value);\n}\n\nexport function formatSignedPct(value: number) {\n  return `${value > 0 ? '+' : value < 0 ? '−' : ''}${Math.abs(value).toFixed(2)}%`;\n}\n\nexport const DATE_SHORT = new Intl.DateTimeFormat('en-US', {\n  month: 'short',\n  day: 'numeric',\n  timeZone: 'UTC',\n});\nexport const DATE_FULL = new Intl.DateTimeFormat('en-US', {\n  month: 'short',\n  day: 'numeric',\n  year: 'numeric',\n  timeZone: 'UTC',\n});\n\nexport function formatAxisPrice(value: number) {\n  if (Math.abs(value) >= 10_000) return formatMoney(value, true);\n  return formatMoney(value, false);\n}\n\nexport function niceTicks(lo: number, hi: number, target = 5): number[] {\n  if (!Number.isFinite(lo) || !Number.isFinite(hi) || hi <= lo) return [lo];\n  const raw = (hi - lo) / target;\n  const mag = Math.pow(10, Math.floor(Math.log10(raw)));\n  const norm = raw / mag;\n  const step = (norm >= 7.5 ? 10 : norm >= 3.5 ? 5 : norm >= 2.25 ? 2.5 : norm >= 1.5 ? 2 : 1) * mag;\n  const first = Math.ceil(lo / step) * step;\n  const out: number[] = [];\n  for (let v = first; v <= hi + step * 0.001; v += step) {\n    out.push(Math.round(v / step) * step);\n  }\n  return out;\n}\n\nexport function monotonePath(points: { x: number; y: number }[]): string {\n  const n = points.length;\n  if (n === 0) return '';\n  if (n === 1) return `M${points[0].x},${points[0].y}`;\n\n  const dx: number[] = [];\n  const slope: number[] = [];\n  for (let i = 0; i < n - 1; i += 1) {\n    dx[i] = points[i + 1].x - points[i].x;\n    slope[i] = dx[i] === 0 ? 0 : (points[i + 1].y - points[i].y) / dx[i];\n  }\n\n  const tangent = new Array<number>(n);\n  tangent[0] = slope[0];\n  tangent[n - 1] = slope[n - 2];\n  for (let i = 1; i < n - 1; i += 1) {\n    if (slope[i - 1] * slope[i] <= 0) {\n      tangent[i] = 0;\n    } else {\n      const w1 = 2 * dx[i] + dx[i - 1];\n      const w2 = dx[i] + 2 * dx[i - 1];\n      tangent[i] = (w1 + w2) / (w1 / slope[i - 1] + w2 / slope[i]);\n    }\n  }\n\n  let d = `M${points[0].x},${points[0].y}`;\n  for (let i = 0; i < n - 1; i += 1) {\n    const c1x = points[i].x + dx[i] / 3;\n    const c1y = points[i].y + (tangent[i] * dx[i]) / 3;\n    const c2x = points[i + 1].x - dx[i] / 3;\n    const c2y = points[i + 1].y - (tangent[i + 1] * dx[i]) / 3;\n    d += `C${c1x.toFixed(2)},${c1y.toFixed(2)} ${c2x.toFixed(2)},${c2y.toFixed(2)} ${points[i + 1].x.toFixed(2)},${points[i + 1].y.toFixed(2)}`;\n  }\n  return d;\n}\n\nexport const MORPH_SAMPLES = 132;\n\nexport function resample(values: number[], n = MORPH_SAMPLES): number[] {\n  if (values.length === 0) return new Array(n).fill(0);\n  if (values.length === 1) return new Array(n).fill(values[0]);\n  const out = new Array<number>(n);\n  for (let i = 0; i < n; i += 1) {\n    const p = (i / (n - 1)) * (values.length - 1);\n    const lo = Math.floor(p);\n    const hi = Math.min(values.length - 1, lo + 1);\n    out[i] = values[lo] + (values[hi] - values[lo]) * (p - lo);\n  }\n  return out;\n}\n\nexport function usePrefersReducedMotion() {\n  const [reduce, setReduce] = React.useState(false);\n  React.useEffect(() => {\n    const mq = window.matchMedia('(prefers-reduced-motion: reduce)');\n    const sync = () => setReduce(mq.matches);\n    sync();\n    mq.addEventListener('change', sync);\n    return () => mq.removeEventListener('change', sync);\n  }, []);\n  return reduce;\n}\n\nexport function useElementWidth<T extends HTMLElement>() {\n  const ref = React.useRef<T | null>(null);\n  const [width, setWidth] = React.useState(0);\n  React.useEffect(() => {\n    const node = ref.current;\n    if (!node) return;\n    const ro = new ResizeObserver(([entry]) => {\n      setWidth(entry.contentRect.width);\n    });\n    ro.observe(node);\n    setWidth(node.getBoundingClientRect().width);\n    return () => ro.disconnect();\n  }, []);\n  return [ref, width] as const;\n}\n\nexport const easeOutCubic = (t: number) => 1 - Math.pow(1 - t, 3);\n\nexport function useTween(target: number[], { duration = 520, enabled = true } = {}) {\n  const [value, setValue] = React.useState(target);\n  const currentRef = React.useRef(target);\n  const fromRef = React.useRef(target);\n  const toRef = React.useRef(target);\n  const startRef = React.useRef(0);\n  const rafRef = React.useRef(0);\n\n  React.useEffect(() => {\n    if (!enabled) return;\n    const to = toRef.current;\n    const changed = to.length !== target.length || target.some((v, i) => v !== to[i]);\n    if (!changed) return;\n\n    toRef.current = target;\n\n    if (currentRef.current.length !== target.length) {\n      currentRef.current = target;\n      fromRef.current = target;\n      cancelAnimationFrame(rafRef.current);\n      rafRef.current = requestAnimationFrame(() => setValue(target));\n      return;\n    }\n\n    fromRef.current = currentRef.current;\n    startRef.current = performance.now();\n    cancelAnimationFrame(rafRef.current);\n\n    const tick = (now: number) => {\n      const p = Math.min(1, (now - startRef.current) / duration);\n      const e = easeOutCubic(p);\n      const from = fromRef.current;\n      const dest = toRef.current;\n      const next = dest.map((v, i) => from[i] + (v - from[i]) * e);\n      currentRef.current = next;\n      setValue(next);\n      if (p < 1) rafRef.current = requestAnimationFrame(tick);\n    };\n    rafRef.current = requestAnimationFrame(tick);\n  });\n\n  React.useEffect(() => () => cancelAnimationFrame(rafRef.current), []);\n\n  if (!enabled || value.length !== target.length) return target;\n  return value;\n}\n\nexport function useTweenNumber(target: number, options?: { duration?: number; enabled?: boolean }) {\n  const vec = React.useMemo(() => [target], [target]);\n  return useTween(vec, options)[0];\n}\n\nexport const KEYFRAMES = `\n@keyframes spectrum-mc-rise {\n  from { transform: scaleY(0); opacity: 0; }\n  to   { transform: scaleY(1); opacity: 1; }\n}\n@keyframes spectrum-mc-draw {\n  from { stroke-dashoffset: 1; }\n  to   { stroke-dashoffset: 0; }\n}\n@keyframes spectrum-mc-fade {\n  from { opacity: 0; }\n  to   { opacity: 1; }\n}\n@keyframes spectrum-sk-pulse {\n  from { opacity: 1; }\n  to   { opacity: 0.4; }\n}\n@keyframes spectrum-mc-enter {\n  from { opacity: 0; transform: translateY(6px); }\n  to   { opacity: 1; transform: translateY(0); }\n}\n@keyframes spectrum-mc-grow {\n  from { transform: scaleX(0); }\n  to   { transform: scaleX(1); }\n}\n@keyframes spectrum-mc-flash {\n  from { opacity: 0.2; }\n  to   { opacity: 0; }\n}\n@keyframes spectrum-mc-ping {\n  0%   { r: 4; opacity: 0.55; }\n  70%  { r: 13; opacity: 0; }\n  100% { r: 13; opacity: 0; }\n}\n`;\n\nexport function Keyframes() {\n  return <style>{KEYFRAMES}</style>;\n}\n\nconst DIGITS = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];\n\nfunction Digit({ char, animate }: { char: string; animate: boolean }) {\n  const digit = char >= '0' && char <= '9' ? Number(char) : null;\n  if (digit == null) {\n    return (\n      <span aria-hidden className=\"inline-block h-[1em] align-bottom leading-none\">\n        {char}\n      </span>\n    );\n  }\n  return (\n    <span\n      aria-hidden\n      className=\"relative inline-block h-[1em] w-[1ch] overflow-hidden align-bottom leading-none\"\n    >\n      <span\n        className=\"absolute inset-x-0 top-0 block\"\n        style={{\n          transform: `translateY(-${digit}em)`,\n          transition: animate ? 'transform 620ms cubic-bezier(0.22, 1, 0.36, 1)' : undefined,\n        }}\n      >\n        {DIGITS.map((d) => (\n          <span key={d} className=\"block h-[1em] text-center leading-none\">\n            {d}\n          </span>\n        ))}\n      </span>\n    </span>\n  );\n}\n\nexport function RollingNumber({\n  value,\n  format,\n  className,\n  animate = true,\n}: {\n  value: number;\n  format: (value: number) => string;\n  className?: string;\n  animate?: boolean;\n}) {\n  const text = format(value);\n  return (\n    <span className={cn('inline-flex items-end leading-none tabular-nums', className)}>\n      <span className=\"sr-only\">{text}</span>\n      {text.split('').map((char, index) => (\n        <Digit key={`${index}-${char >= '0' && char <= '9' ? 'digit' : char}`} char={char} animate={animate} />\n      ))}\n    </span>\n  );\n}\n\nexport function RangeSelector({\n  ranges,\n  value,\n  onChange,\n  reduce,\n}: {\n  ranges: MarketRange[];\n  value: string;\n  onChange: (label: string) => void;\n  reduce: boolean;\n}) {\n  const index = Math.max(0, ranges.findIndex((r) => r.label === value));\n  const width = 100 / ranges.length;\n\n  return (\n    <div\n      role=\"tablist\"\n      aria-label=\"Time range\"\n      className=\"relative inline-flex items-center rounded-full bg-black/[0.045] p-0.5 dark:bg-white/[0.07]\"\n    >\n      <span\n        aria-hidden\n        className=\"absolute inset-y-0.5 left-0.5 rounded-full bg-white shadow-sm ring-1 ring-black/[0.06] dark:bg-white/12 dark:ring-white/10\"\n        style={{\n          width: `calc(${width}% - 4px)`,\n          transform: `translateX(calc(${index * 100}% + ${index * 4}px))`,\n          transition: reduce ? undefined : 'transform 380ms cubic-bezier(0.22, 1, 0.36, 1)',\n        }}\n      />\n      {ranges.map((range) => {\n        const active = range.label === value;\n        return (\n          <button\n            key={range.label}\n            type=\"button\"\n            role=\"tab\"\n            aria-selected={active}\n            onClick={() => onChange(range.label)}\n            className={cn(\n              'relative z-10 rounded-full px-2.5 py-1 font-mono text-[11px] leading-none tracking-wide transition-colors duration-200',\n              'focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-black/20 dark:focus-visible:ring-white/25',\n              active\n                ? 'text-neutral-950 dark:text-white'\n                : 'text-neutral-500 hover:text-neutral-800 dark:text-neutral-400 dark:hover:text-neutral-200',\n            )}\n            style={{ flex: `0 0 ${width}%` }}\n          >\n            {range.label}\n          </button>\n        );\n      })}\n    </div>\n  );\n}\n\nexport type BookLevel = {\n  price: number;\n  size: number;\n  total: number;\n};\n\nexport type OrderBook = {\n  mid: number;\n  spread: number;\n  bids: BookLevel[];\n  asks: BookLevel[];\n};\n\nexport function generateOrderBook({\n  mid,\n  seed,\n  levels = 14,\n  tick,\n  depth = 900,\n}: {\n  mid: number;\n  seed: number;\n  levels?: number;\n  tick?: number;\n  depth?: number;\n}): OrderBook {\n  const rand = mulberry32(seed);\n  const step = tick ?? Math.max(0.01, round2(mid * 0.0006));\n\n  const side = (direction: 1 | -1): BookLevel[] => {\n    const out: BookLevel[] = [];\n    let total = 0;\n    for (let i = 0; i < levels; i += 1) {\n      const distance = (i + 1) / levels;\n      const base = depth * (0.18 + distance * 0.9);\n      const whale = rand() > 0.87 ? 2.4 : 1;\n      const size = Math.round(base * (0.55 + rand() * 0.9) * whale);\n      total += size;\n      out.push({\n        price: round2(mid + direction * step * (i + 1)),\n        size,\n        total,\n      });\n    }\n    return out;\n  };\n\n  const asks = side(1);\n  const bids = side(-1);\n  return { mid, spread: round2(asks[0].price - bids[0].price), bids, asks };\n}\n\nexport type TreemapInput = { label: string; weight: number; change: number; name?: string };\nexport type TreemapTile = TreemapInput & { x: number; y: number; w: number; h: number };\n\nexport function squarify(\n  items: TreemapInput[],\n  width: number,\n  height: number,\n): TreemapTile[] {\n  const sorted = [...items].filter((i) => i.weight > 0).sort((a, b) => b.weight - a.weight);\n  const totalWeight = sorted.reduce((sum, i) => sum + i.weight, 0);\n  if (!sorted.length || totalWeight <= 0 || width <= 0 || height <= 0) return [];\n\n  const scale = (width * height) / totalWeight;\n  const out: TreemapTile[] = [];\n  let x = 0;\n  let y = 0;\n  let w = width;\n  let h = height;\n  let row: TreemapInput[] = [];\n  let index = 0;\n\n  const worst = (candidate: TreemapInput[], side: number) => {\n    if (!candidate.length || side <= 0) return Infinity;\n    const areas = candidate.map((i) => i.weight * scale);\n    const sum = areas.reduce((a, b) => a + b, 0);\n    const max = Math.max(...areas);\n    const min = Math.min(...areas);\n    const side2 = side * side;\n    const sum2 = sum * sum;\n    return Math.max((side2 * max) / sum2, sum2 / (side2 * min));\n  };\n\n  const layoutRow = (candidate: TreemapInput[], side: number, horizontal: boolean) => {\n    const sum = candidate.reduce((total, i) => total + i.weight * scale, 0);\n    const thickness = sum / side;\n    let offset = 0;\n    for (const item of candidate) {\n      const length = (item.weight * scale) / thickness;\n      out.push(\n        horizontal\n          ? { ...item, x: x + offset, y, w: length, h: thickness }\n          : { ...item, x, y: y + offset, w: thickness, h: length },\n      );\n      offset += length;\n    }\n    if (horizontal) {\n      y += thickness;\n      h -= thickness;\n    } else {\n      x += thickness;\n      w -= thickness;\n    }\n  };\n\n  while (index < sorted.length) {\n    const horizontal = w >= h;\n    const side = horizontal ? w : h;\n    const next = sorted[index];\n\n    if (!row.length || worst([...row, next], side) <= worst(row, side)) {\n      row.push(next);\n      index += 1;\n    } else {\n      layoutRow(row, side, horizontal);\n      row = [];\n    }\n  }\n  if (row.length) layoutRow(row, w >= h ? w : h, w >= h);\n\n  return out;\n}\n\nexport function changeColor(change: number, cap = 4) {\n  const t = Math.max(-1, Math.min(1, change / cap));\n  if (Math.abs(t) < 0.04) return 'var(--spectrum-heat-flat)';\n  const weight = 0.22 + Math.abs(t) * 0.78;\n  const base = t > 0 ? UP : DOWN;\n  return `color-mix(in srgb, ${base} ${(weight * 100).toFixed(0)}%, var(--spectrum-heat-flat))`;\n}\n\nexport const seriesVarsClassName =\n  '[--spectrum-series-1:#2563eb] [--spectrum-series-2:#f59e0b] [--spectrum-series-3:#0d9488] [--spectrum-series-4:#7c3aed] [--spectrum-series-5:#db2777] [--spectrum-series-6:#64748b] [--spectrum-track:#ececef] [--spectrum-chart-surface:#fff] [--spectrum-chart-up:#059669] [--spectrum-chart-down:#e11d48] dark:[--spectrum-series-1:#60a5fa] dark:[--spectrum-series-2:#fbbf24] dark:[--spectrum-series-3:#2dd4bf] dark:[--spectrum-series-4:#a78bfa] dark:[--spectrum-series-5:#f472b6] dark:[--spectrum-series-6:#94a3b8] dark:[--spectrum-track:#26262b] dark:[--spectrum-chart-surface:#0a0a0a] dark:[--spectrum-chart-up:#34d399] dark:[--spectrum-chart-down:#fb7185]';\n\nexport const SERIES_COLORS = [\n  'var(--spectrum-series-1)',\n  'var(--spectrum-series-2)',\n  'var(--spectrum-series-3)',\n  'var(--spectrum-series-4)',\n  'var(--spectrum-series-5)',\n  'var(--spectrum-series-6)',\n] as const;\n\nexport const TRACK = 'var(--spectrum-track)';\n\nexport const textHalo = {\n  paintOrder: 'stroke',\n  stroke: SURFACE,\n  strokeWidth: 3.5,\n  strokeLinejoin: 'round',\n} as const satisfies React.CSSProperties;\n\nexport function intensityColor(t: number, hue = 'var(--spectrum-series-1)') {\n  const clamped = Math.max(0, Math.min(1, t));\n  if (clamped <= 0.001) return TRACK;\n  const weight = 14 + clamped * 86;\n  return `color-mix(in srgb, ${hue} ${weight.toFixed(0)}%, ${TRACK})`;\n}\n\nexport function formatCount(value: number, digits = 1) {\n  if (Math.abs(value) < 1000) return String(Math.round(value));\n  return new Intl.NumberFormat('en-US', {\n    notation: 'compact',\n    maximumFractionDigits: digits,\n  }).format(value);\n}\n\nexport function formatPct(value: number, digits = 1) {\n  return `${value.toFixed(digits)}%`;\n}\n\nexport function onFillClass(intensity: number) {\n  return intensity > 0.55\n    ? 'fill-white dark:fill-neutral-950'\n    : 'fill-neutral-900 dark:fill-white';\n}\n\nexport type ChartStatus = 'ready' | 'loading' | 'empty' | 'error';\n\nexport type SkeletonVariant = 'bars' | 'line' | 'grid' | 'rows' | 'arc' | 'cards';\n\nconst SKELETON_BARS = [46, 72, 55, 83, 41, 68, 92, 57, 76, 49, 88, 63];\nconst SKELETON_ROWS = [92, 74, 86, 61, 79, 55];\n\nfunction SkeletonShapes({\n  variant,\n  height,\n  mode,\n  reduce,\n}: {\n  variant: SkeletonVariant;\n  height: number;\n  mode: 'pulse' | 'ghost';\n  reduce: boolean;\n}) {\n  const animate = mode === 'pulse' && !reduce;\n  const breathe = (index: number): React.CSSProperties | undefined =>\n    animate\n      ? { animation: `spectrum-sk-pulse 1.5s ease-in-out ${index * 120}ms infinite alternate` }\n      : undefined;\n  const block = 'rounded-md bg-black/[0.06] dark:bg-white/[0.08]';\n\n  if (variant === 'bars') {\n    return (\n      <div className=\"flex h-full items-end gap-2 pb-5 pt-3\">\n        {SKELETON_BARS.map((h, i) => (\n          <div key={i} className={cn('flex-1', block)} style={{ height: `${h}%`, ...breathe(i) }} />\n        ))}\n      </div>\n    );\n  }\n\n  if (variant === 'line') {\n    return (\n      <div className=\"relative flex h-full flex-col justify-between py-4\">\n        {[0, 1, 2, 3].map((i) => (\n          <div key={i} className=\"h-px w-full bg-black/[0.05] dark:bg-white/[0.06]\" />\n        ))}\n        <svg\n          className=\"absolute inset-x-0 top-1/4 h-1/2 w-full\"\n          preserveAspectRatio=\"none\"\n          viewBox=\"0 0 100 40\"\n        >\n          <path\n            d=\"M0,30 C12,10 22,34 34,22 C46,10 54,28 66,16 C78,6 88,20 100,8\"\n            fill=\"none\"\n            className=\"stroke-black/[0.1] dark:stroke-white/[0.12]\"\n            strokeWidth={2}\n            vectorEffect=\"non-scaling-stroke\"\n            style={breathe(1)}\n          />\n        </svg>\n      </div>\n    );\n  }\n\n  if (variant === 'grid') {\n    return (\n      <div className=\"grid h-full grid-cols-12 grid-rows-6 gap-1.5 py-2\">\n        {Array.from({ length: 72 }, (_, i) => (\n          <div\n            key={i}\n            className={cn('h-full w-full rounded-[3px]', block)}\n            style={breathe(i % 14)}\n          />\n        ))}\n      </div>\n    );\n  }\n\n  if (variant === 'rows') {\n    return (\n      <div className=\"flex h-full flex-col justify-evenly py-2\">\n        {SKELETON_ROWS.map((w, i) => (\n          <div key={i} className={cn('h-4', block)} style={{ width: `${w}%`, ...breathe(i) }} />\n        ))}\n      </div>\n    );\n  }\n\n  if (variant === 'cards') {\n    return (\n      <div className=\"grid h-full grid-cols-2 content-center gap-3 py-2 sm:grid-cols-4\">\n        {[0, 1, 2, 3].map((i) => (\n          <div\n            key={i}\n            className=\"flex h-full min-h-16 flex-col justify-between gap-2 rounded-xl border border-black/[0.05] p-3 dark:border-white/[0.06]\"\n            style={breathe(i)}\n          >\n            <div className={cn('h-2.5 w-1/2', block)} />\n            <div className={cn('h-5 w-3/4', block)} />\n            <div className={cn('h-6 w-full', block)} />\n          </div>\n        ))}\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"flex h-full items-center justify-center\">\n      <div\n        className=\"rounded-full border-[14px] border-black/[0.06] dark:border-white/[0.08]\"\n        style={{ width: height * 0.6, height: height * 0.6, ...breathe(0) }}\n      />\n    </div>\n  );\n}\n\nexport function ChartSkeleton({\n  variant = 'bars',\n  height = 300,\n  className,\n}: {\n  variant?: SkeletonVariant;\n  height?: number;\n  className?: string;\n}) {\n  const reduce = usePrefersReducedMotion();\n  return (\n    <div\n      className={cn('relative w-full overflow-hidden', className)}\n      style={{ height }}\n      aria-hidden\n    >\n      <Keyframes />\n      <SkeletonShapes variant={variant} height={height} mode=\"pulse\" reduce={reduce} />\n    </div>\n  );\n}\n\nfunction StateShell({\n  height,\n  variant,\n  icon,\n  iconClassName,\n  title,\n  description,\n  action,\n}: {\n  height: number;\n  variant: SkeletonVariant;\n  icon: React.ReactNode;\n  iconClassName?: string;\n  title: string;\n  description?: string;\n  action?: React.ReactNode;\n}) {\n  const reduce = usePrefersReducedMotion();\n  return (\n    <div\n      className=\"relative flex w-full items-center justify-center overflow-hidden rounded-xl border border-black/[0.06] bg-black/[0.015] dark:border-white/[0.08] dark:bg-white/[0.02]\"\n      style={{ height }}\n      role=\"status\"\n    >\n      <Keyframes />\n      <div\n        aria-hidden\n        className=\"pointer-events-none absolute inset-x-5 inset-y-4 opacity-60 dark:opacity-50\"\n        style={{\n          maskImage:\n            'radial-gradient(ellipse 62% 58% at 50% 50%, transparent 34%, black 78%)',\n          WebkitMaskImage:\n            'radial-gradient(ellipse 62% 58% at 50% 50%, transparent 34%, black 78%)',\n        }}\n      >\n        <SkeletonShapes variant={variant} height={height} mode=\"ghost\" reduce={reduce} />\n      </div>\n      <div\n        className=\"relative flex max-w-[38ch] flex-col items-center gap-1 px-6 text-center\"\n        style={reduce ? undefined : { animation: `spectrum-mc-enter 480ms ${EASE} both` }}\n      >\n        <span\n          className={cn(\n            'mb-2 flex size-10 items-center justify-center rounded-xl border border-black/[0.07] bg-white text-neutral-500 shadow-xs dark:border-white/[0.1] dark:bg-neutral-900 dark:text-neutral-400',\n            iconClassName,\n          )}\n        >\n          {icon}\n        </span>\n        <p className=\"text-[13px] font-medium text-neutral-900 dark:text-neutral-100\">{title}</p>\n        {description ? (\n          <p className=\"text-[12px] leading-relaxed text-neutral-500 dark:text-neutral-400\">\n            {description}\n          </p>\n        ) : null}\n        {action ? <div className=\"mt-3\">{action}</div> : null}\n      </div>\n    </div>\n  );\n}\n\nconst actionClass =\n  'inline-flex items-center gap-1.5 rounded-full border border-black/10 bg-white px-3 py-1.5 text-[12px] font-medium text-neutral-900 shadow-xs transition-colors hover:bg-black/[0.03] focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-black/20 dark:border-white/12 dark:bg-white/[0.06] dark:text-white dark:hover:bg-white/[0.1] dark:focus-visible:ring-white/25';\n\nexport function ChartEmpty({\n  height = 300,\n  variant = 'line',\n  title = 'No data yet',\n  description = 'Once events start arriving this chart will fill in automatically.',\n  action,\n}: {\n  height?: number;\n  variant?: SkeletonVariant;\n  title?: string;\n  description?: string;\n  action?: React.ReactNode;\n}) {\n  return (\n    <StateShell\n      height={height}\n      variant={variant}\n      title={title}\n      description={description}\n      action={action}\n      icon={\n        <svg width=\"17\" height=\"17\" viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden>\n          <path\n            d=\"M4 19h16M7 16V9m5 7V5m5 11v-4\"\n            stroke=\"currentColor\"\n            strokeWidth=\"1.7\"\n            strokeLinecap=\"round\"\n          />\n        </svg>\n      }\n    />\n  );\n}\n\nexport function ChartError({\n  height = 300,\n  variant = 'line',\n  title = 'Could not load this chart',\n  description = 'The request failed. Check the connection and try again.',\n  onRetry,\n  retryLabel = 'Retry',\n}: {\n  height?: number;\n  variant?: SkeletonVariant;\n  title?: string;\n  description?: string;\n  onRetry?: () => void;\n  retryLabel?: string;\n}) {\n  return (\n    <StateShell\n      height={height}\n      variant={variant}\n      title={title}\n      description={description}\n      iconClassName=\"text-rose-500/90 dark:text-rose-400/90\"\n      action={\n        onRetry ? (\n          <button type=\"button\" onClick={onRetry} className={actionClass}>\n            <svg width=\"13\" height=\"13\" viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden>\n              <path\n                d=\"M20 11A8 8 0 1 0 18 16M20 5v6h-6\"\n                stroke=\"currentColor\"\n                strokeWidth=\"1.9\"\n                strokeLinecap=\"round\"\n                strokeLinejoin=\"round\"\n              />\n            </svg>\n            {retryLabel}\n          </button>\n        ) : null\n      }\n      icon={\n        <svg width=\"17\" height=\"17\" viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden>\n          <path\n            d=\"M12 8v5m0 3.5v.5M10.3 3.9 2.6 17.3A2 2 0 0 0 4.3 20h15.4a2 2 0 0 0 1.7-2.7L13.7 3.9a2 2 0 0 0-3.4 0Z\"\n            stroke=\"currentColor\"\n            strokeWidth=\"1.6\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n          />\n        </svg>\n      }\n    />\n  );\n}\n\nexport function ChartState({\n  status = 'ready',\n  height = 300,\n  variant = 'bars',\n  empty,\n  error,\n  onRetry,\n  children,\n}: {\n  status?: ChartStatus;\n  height?: number;\n  variant?: SkeletonVariant;\n  empty?: { title?: string; description?: string; action?: React.ReactNode };\n  error?: { title?: string; description?: string; retryLabel?: string };\n  onRetry?: () => void;\n  children: React.ReactNode;\n}) {\n  if (status === 'loading') {\n    return (\n      <div aria-busy=\"true\" aria-live=\"polite\">\n        <ChartSkeleton variant={variant} height={height} />\n        <span className=\"sr-only\">Loading chart data</span>\n      </div>\n    );\n  }\n  if (status === 'empty') return <ChartEmpty height={height} variant={variant} {...empty} />;\n  if (status === 'error') {\n    return <ChartError height={height} variant={variant} onRetry={onRetry} {...error} />;\n  }\n  return <>{children}</>;\n}\n\nexport function useHoverIndexKeys({\n  count,\n  setIndex,\n  clear,\n}: {\n  count: number;\n  setIndex: React.Dispatch<React.SetStateAction<number | null>>;\n  clear?: () => void;\n}) {\n  return React.useCallback(\n    (event: React.KeyboardEvent) => {\n      const { key } = event;\n      if (key === 'Escape') {\n        event.preventDefault();\n        if (clear) clear();\n        else setIndex(null);\n        return;\n      }\n      if (key === 'Home' || key === 'End') {\n        event.preventDefault();\n        setIndex(key === 'Home' ? 0 : count - 1);\n        return;\n      }\n      if (key !== 'ArrowLeft' && key !== 'ArrowRight') return;\n      event.preventDefault();\n      const step = key === 'ArrowRight' ? 1 : -1;\n      setIndex((current) => {\n        const next = (current ?? count - 1) + step;\n        return Math.max(0, Math.min(count - 1, next));\n      });\n    },\n    [count, setIndex, clear],\n  );\n}\n\nexport function ChartDataTable({\n  caption,\n  columns,\n  rows,\n}: {\n  caption: string;\n  columns: string[];\n  rows: (string | number)[][];\n}) {\n  return (\n    <table className=\"sr-only\">\n      <caption>{caption}</caption>\n      <thead>\n        <tr>\n          {columns.map((column) => (\n            <th key={column} scope=\"col\">\n              {column}\n            </th>\n          ))}\n        </tr>\n      </thead>\n      <tbody>\n        {rows.map((row, index) => (\n          <tr key={index}>\n            {row.map((cell, cellIndex) =>\n              cellIndex === 0 ? (\n                <th key={cellIndex} scope=\"row\">\n                  {cell}\n                </th>\n              ) : (\n                <td key={cellIndex}>{cell}</td>\n              ),\n            )}\n          </tr>\n        ))}\n      </tbody>\n    </table>\n  );\n}\n\nexport function Stat({ ready, children }: { ready: boolean; children: React.ReactNode }) {\n  if (ready) return <>{children}</>;\n  return (\n    <span aria-hidden className=\"text-neutral-300 dark:text-neutral-600\">\n      —\n    </span>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/spectrumui/charts/chart-engine.tsx"
    },
    {
      "path": "app/registry/charts/market-chart.tsx",
      "content": "'use client';\n\nimport * as React from 'react';\nimport { cn } from '@/lib/utils';\nimport {\n  type ChartStatus,\n  ChartState,\n  AAPL_MARKET,\n  BTC_MARKET,\n  type Candle,\n  DAY_MS,\n  Keyframes,\n  MARKET_RANGES,\n  type MarketRange,\n  MORPH_SAMPLES,\n  RollingNumber,\n  RangeSelector,\n  SOL_MARKET,\n  DATE_FULL,\n  DATE_SHORT,\n  formatAxisPrice,\n  formatMoney,\n  formatSignedPct,\n  marketVarsClassName,\n  monotonePath,\n  mulberry32,\n  niceTicks,\n  resample,\n  round2,\n  useElementWidth,\n  usePrefersReducedMotion,\n  useTween,\n  useTweenNumber,\n} from './chart-engine';\n\nconst NO_TICKS: Candle[] = [];\n\nconst PAD = { top: 12, right: 60, bottom: 22, left: 12 };\nconst VOLUME_SHARE = 0.2;\nconst VOLUME_GAP = 12;\n\nexport interface MarketChartProps {\n  className?: string;\n  data?: Candle[];\n  symbol?: string;\n  name?: string;\n  variant?: 'candles' | 'area';\n  showVolume?: boolean;\n  hollowUp?: boolean;\n  ranges?: MarketRange[];\n  defaultRange?: string;\n  showRangeSelector?: boolean;\n  live?: boolean;\n  height?: number;\n  compactPrice?: boolean;\n  status?: ChartStatus;\n  onRetry?: () => void;\n}\n\nexport function MarketChart({\n  className,\n  data = SOL_MARKET,\n  symbol = 'SOL',\n  name = 'Solana',\n  variant = 'candles',\n  showVolume = true,\n  hollowUp = false,\n  ranges = MARKET_RANGES,\n  defaultRange = '3M',\n  showRangeSelector = true,\n  live = false,\n  height = 380,\n  compactPrice = false,\n  status = 'ready',\n  onRetry,\n}: MarketChartProps) {\n  const reduce = usePrefersReducedMotion();\n  const [wrapRef, width] = useElementWidth<HTMLDivElement>();\n  const [rangeLabel, setRangeLabel] = React.useState(\n    () => ranges.find((r) => r.label === defaultRange)?.label ?? ranges[ranges.length - 1].label,\n  );\n  const [hoverIndex, setHoverIndex] = React.useState<number | null>(null);\n  const [hoverY, setHoverY] = React.useState<number | null>(null);\n\n  const [tickState, setTicks] = React.useState<Candle[]>([]);\n  const ticks = live ? tickState : NO_TICKS;\n  const [flash, setFlash] = React.useState<'up' | 'down' | null>(null);\n\n  React.useEffect(() => {\n    if (!live) return;\n    const rand = mulberry32(0x5eed);\n    let last = data[data.length - 1];\n    const id = window.setInterval(() => {\n      const shock = (rand() - 0.48) * 0.018;\n      const close = round2(Math.max(0.01, last.close * (1 + shock)));\n      const next: Candle = {\n        t: last.t + DAY_MS,\n        open: last.close,\n        high: round2(Math.max(last.close, close) * (1 + rand() * 0.006)),\n        low: round2(Math.min(last.close, close) * (1 - rand() * 0.006)),\n        close,\n        volume: Math.round((0.6 + rand()) * 1_000_000),\n      };\n      last = next;\n      setFlash(close >= next.open ? 'up' : 'down');\n      setTicks((prev) => [...prev, next].slice(-120));\n    }, 1600);\n    return () => window.clearInterval(id);\n  }, [live, data]);\n\n  React.useEffect(() => {\n    if (!flash) return;\n    const id = window.setTimeout(() => setFlash(null), 620);\n    return () => window.clearTimeout(id);\n  }, [flash, ticks.length]);\n\n  const series = React.useMemo(() => (ticks.length ? [...data, ...ticks] : data), [data, ticks]);\n\n  const range = ranges.find((r) => r.label === rangeLabel) ?? ranges[ranges.length - 1];\n  const view = React.useMemo(() => {\n    const bars = range.bars;\n    if (bars == null || bars >= series.length) return series;\n    return series.slice(series.length - bars);\n  }, [series, range.bars]);\n\n  const n = view.length;\n\n  const w = Math.max(width, 260);\n  const h = height;\n  const plotX0 = PAD.left;\n  const plotX1 = w - PAD.right;\n  const plotW = Math.max(1, plotX1 - plotX0);\n  const innerTop = PAD.top;\n  const innerBottom = h - PAD.bottom;\n  const volH = showVolume ? Math.round((innerBottom - innerTop) * VOLUME_SHARE) : 0;\n  const priceY0 = innerTop;\n  const priceY1 = innerBottom - (showVolume ? volH + VOLUME_GAP : 0);\n  const volY0 = innerBottom - volH;\n  const volY1 = innerBottom;\n  const priceH = Math.max(1, priceY1 - priceY0);\n\n  const step = plotW / Math.max(n, 1);\n  const bodyW = Math.max(1, Math.min(step * 0.68, 18));\n  const cx = React.useCallback((i: number) => plotX0 + step * (i + 0.5), [plotX0, step]);\n\n  const rawDomain = React.useMemo(() => {\n    if (!n) return [0, 1] as const;\n    let lo = Infinity;\n    let hi = -Infinity;\n    for (const c of view) {\n      const low = variant === 'area' ? c.close : c.low;\n      const high = variant === 'area' ? c.close : c.high;\n      if (low < lo) lo = low;\n      if (high > hi) hi = high;\n    }\n    const pad = (hi - lo) * 0.12 || Math.abs(hi) * 0.02 || 1;\n    return [lo - pad, hi + pad] as const;\n  }, [view, n, variant]);\n\n  const [domLo, domHi] = useTween([rawDomain[0], rawDomain[1]], {\n    duration: 560,\n    enabled: !reduce,\n  });\n\n  const priceY = React.useCallback(\n    (v: number) => {\n      const span = domHi - domLo || 1;\n      return priceY1 - ((v - domLo) / span) * priceH;\n    },\n    [domHi, domLo, priceY1, priceH],\n  );\n\n  const priceAt = React.useCallback(\n    (y: number) => domLo + ((priceY1 - y) / priceH) * (domHi - domLo),\n    [domLo, domHi, priceY1, priceH],\n  );\n\n  const maxVolume = React.useMemo(\n    () => view.reduce((max, c) => Math.max(max, c.volume), 1),\n    [view],\n  );\n\n  const closes = React.useMemo(() => view.map((c) => c.close), [view]);\n  const morphed = useTween(\n    React.useMemo(() => resample(closes), [closes]),\n    { duration: 560, enabled: !reduce && variant === 'area' },\n  );\n\n  const first = view[0];\n  const last = view[n - 1];\n  const active = hoverIndex != null ? view[hoverIndex] : null;\n  const shown = active ?? last;\n  const baseline = first?.close ?? 0;\n  const delta = baseline ? ((shown?.close ?? 0) - baseline) / baseline * 100 : 0;\n  const up = delta >= 0;\n  const dirColor = up ? 'var(--spectrum-chart-up)' : 'var(--spectrum-chart-down)';\n\n  const lastUp = last ? last.close >= last.open : true;\n  const lastColor = lastUp ? 'var(--spectrum-chart-up)' : 'var(--spectrum-chart-down)';\n\n  const displayPrice = useTweenNumber(shown?.close ?? 0, {\n    duration: 260,\n    enabled: !reduce && hoverIndex == null,\n  });\n\n  const svgRef = React.useRef<SVGSVGElement | null>(null);\n\n  const pointerToIndex = React.useCallback(\n    (clientX: number, clientY: number) => {\n      const svg = svgRef.current;\n      if (!svg) return;\n      const box = svg.getBoundingClientRect();\n      const x = ((clientX - box.left) / box.width) * w;\n      const y = ((clientY - box.top) / box.height) * h;\n      const i = Math.floor((x - plotX0) / step);\n      setHoverIndex(Math.max(0, Math.min(n - 1, i)));\n      setHoverY(Math.max(priceY0, Math.min(priceY1, y)));\n    },\n    [w, h, plotX0, step, n, priceY0, priceY1],\n  );\n\n  const clearHover = React.useCallback(() => {\n    setHoverIndex(null);\n    setHoverY(null);\n  }, []);\n\n  const onKeyDown = React.useCallback(\n    (event: React.KeyboardEvent) => {\n      if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight' && event.key !== 'Escape') return;\n      event.preventDefault();\n      if (event.key === 'Escape') return clearHover();\n      const dir = event.key === 'ArrowRight' ? 1 : -1;\n      setHoverIndex((current) => {\n        const next = (current ?? n - 1) + dir;\n        return Math.max(0, Math.min(n - 1, next));\n      });\n      setHoverY(null);\n    },\n    [n, clearHover],\n  );\n\n  const priceTicks = React.useMemo(() => niceTicks(domLo, domHi, 5), [domLo, domHi]);\n\n  const timeTicks = React.useMemo(() => {\n    if (!n) return [];\n    const want = Math.max(2, Math.min(6, Math.floor(plotW / 96)));\n    const gap = Math.max(1, Math.floor((n - 1) / (want - 1 || 1)));\n    const out: number[] = [];\n    for (let i = 0; i < n; i += gap) out.push(i);\n    const minGap = 62;\n    const xOf = (i: number) => plotX0 + (plotW / Math.max(n, 1)) * (i + 0.5);\n    if (out[out.length - 1] !== n - 1) {\n      while (out.length && xOf(n - 1) - xOf(out[out.length - 1]) < minGap) out.pop();\n      out.push(n - 1);\n    }\n    return out;\n  }, [n, plotW, plotX0]);\n\n  const uid = React.useId().replace(/:/g, '');\n  const ready = width > 0;\n\n  const areaPoints = React.useMemo(() => {\n    const values = morphed.length === MORPH_SAMPLES ? morphed : resample(closes);\n    return values.map((v, i) => ({\n      x: plotX0 + (i / (values.length - 1 || 1)) * plotW,\n      y: priceY(v),\n    }));\n  }, [morphed, closes, plotX0, plotW, priceY]);\n\n  const linePath = React.useMemo(\n    () => (variant === 'area' ? monotonePath(areaPoints) : ''),\n    [variant, areaPoints],\n  );\n  const baselineY = priceY(baseline);\n\n  const areaPath = React.useMemo(() => {\n    if (variant !== 'area' || !linePath) return '';\n    return `${linePath}L${plotX1},${baselineY}L${plotX0},${baselineY}Z`;\n  }, [variant, linePath, plotX0, plotX1, baselineY]);\n\n  const staggerFor = (i: number) => (n <= 1 ? 0 : (i / (n - 1)) * 340);\n  const introKey = `${rangeLabel}-${variant}`;\n\n  return (\n    <div\n      ref={wrapRef}\n      className={cn(\n        'flex w-full flex-col text-neutral-400 dark:text-neutral-500',\n        marketVarsClassName,\n        className,\n      )}\n    >\n      <Keyframes />\n\n      <div className=\"mb-3 flex flex-wrap items-start justify-between gap-x-4 gap-y-3\">\n        <div className=\"min-w-0\">\n          <div className=\"flex items-baseline gap-2\">\n            <span className=\"font-mono text-[12px] font-medium tracking-wide text-neutral-950 dark:text-white\">\n              {symbol}\n            </span>\n            <span className=\"truncate text-[12px] text-neutral-500 dark:text-neutral-400\">{name}</span>\n            {live ? (\n              <span className=\"inline-flex items-center gap-1 rounded-full bg-black/[0.05] px-1.5 py-0.5 font-mono text-[9px] uppercase tracking-widest text-neutral-500 dark:bg-white/[0.08] dark:text-neutral-400\">\n                <span\n                  className=\"size-1.5 rounded-full\"\n                  style={{\n                    background: 'var(--spectrum-chart-up)',\n                    animation: reduce ? undefined : 'spectrum-mc-fade 1.1s ease-in-out infinite alternate',\n                  }}\n                />\n                Live\n              </span>\n            ) : null}\n          </div>\n\n          <div className=\"mt-1 flex items-end gap-2.5\">\n            <RollingNumber\n              value={hoverIndex != null ? (shown?.close ?? 0) : displayPrice}\n              format={(v) => formatMoney(v, compactPrice)}\n              animate={!reduce}\n              className={cn(\n                'font-mono text-[26px] font-medium text-neutral-950 transition-colors duration-300 dark:text-white',\n              )}\n            />\n            <span\n              className=\"mb-1 rounded-md px-1.5 py-0.5 font-mono text-[12px] tabular-nums transition-colors duration-300\"\n              style={{\n                color: dirColor,\n                background: flash\n                  ? `color-mix(in srgb, ${dirColor} 16%, transparent)`\n                  : 'transparent',\n              }}\n            >\n              {formatSignedPct(delta)}\n            </span>\n          </div>\n\n          <div\n            className=\"mt-1.5 flex h-4 flex-wrap items-center gap-x-3 font-mono text-[10.5px] tabular-nums transition-opacity duration-200\"\n            style={{ opacity: active ? 1 : 0 }}\n            aria-hidden={!active}\n          >\n            {active ? (\n              <>\n                <span className=\"text-neutral-400 dark:text-neutral-500\">\n                  {DATE_FULL.format(active.t)}\n                </span>\n                {(['open', 'high', 'low', 'close'] as const).map((key) => (\n                  <span key={key} className=\"text-neutral-500 dark:text-neutral-400\">\n                    {key[0].toUpperCase()}{' '}\n                    <span\n                      className=\"text-neutral-950 dark:text-white\"\n                      style={{ color: key === 'close' ? dirColor : undefined }}\n                    >\n                      {formatMoney(active[key], compactPrice)}\n                    </span>\n                  </span>\n                ))}\n                <span className=\"text-neutral-500 dark:text-neutral-400\">\n                  V{' '}\n                  <span className=\"text-neutral-950 dark:text-white\">\n                    {formatMoney(active.volume, true).replace('$', '')}\n                  </span>\n                </span>\n              </>\n            ) : null}\n          </div>\n        </div>\n\n        {showRangeSelector ? (\n          <RangeSelector\n            ranges={ranges}\n            value={rangeLabel}\n            onChange={setRangeLabel}\n            reduce={reduce}\n          />\n        ) : null}\n      </div>\n\n      <ChartState\n        status={status}\n        height={height}\n        variant=\"bars\"\n        empty={{ title: 'No price data', description: 'Point the chart at a candle feed and it will render as soon as bars arrive.' }}\n        onRetry={onRetry}\n      >\n      <div className=\"relative w-full\" style={{ height }}>\n        {!ready ? null : (\n          <svg\n            ref={svgRef}\n            width={w}\n            height={h}\n            viewBox={`0 0 ${w} ${h}`}\n            className=\"block w-full touch-pan-y select-none overflow-visible\"\n            role=\"img\"\n            aria-label={`${symbol} ${name} price chart, ${rangeLabel} range. ${formatMoney(\n              last?.close ?? 0,\n            )}, ${formatSignedPct(delta)}.`}\n            tabIndex={0}\n            onKeyDown={onKeyDown}\n            onPointerMove={(e) => pointerToIndex(e.clientX, e.clientY)}\n            onPointerDown={(e) => pointerToIndex(e.clientX, e.clientY)}\n            onPointerLeave={clearHover}\n            onBlur={clearHover}\n          >\n            <defs>\n              <linearGradient id={`${uid}-up`} x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n                <stop offset=\"0%\" stopColor=\"var(--spectrum-chart-up)\" stopOpacity={0.28} />\n                <stop offset=\"100%\" stopColor=\"var(--spectrum-chart-up)\" stopOpacity={0} />\n              </linearGradient>\n              <linearGradient id={`${uid}-down`} x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n                <stop offset=\"0%\" stopColor=\"var(--spectrum-chart-down)\" stopOpacity={0} />\n                <stop offset=\"100%\" stopColor=\"var(--spectrum-chart-down)\" stopOpacity={0.28} />\n              </linearGradient>\n              <clipPath id={`${uid}-above`}>\n                <rect x={plotX0} y={priceY0 - 40} width={plotW} height={Math.max(0, baselineY - priceY0 + 40)} />\n              </clipPath>\n              <clipPath id={`${uid}-below`}>\n                <rect x={plotX0} y={baselineY} width={plotW} height={Math.max(0, priceY1 - baselineY + 40)} />\n              </clipPath>\n            </defs>\n\n            <g shapeRendering=\"crispEdges\">\n              {priceTicks.map((tick) => {\n                const y = priceY(tick);\n                if (y < priceY0 - 1 || y > priceY1 + 1) return null;\n                return (\n                  <line\n                    key={tick}\n                    x1={plotX0}\n                    x2={plotX1}\n                    y1={y}\n                    y2={y}\n                    stroke=\"currentColor\"\n                    strokeOpacity={0.13}\n                    strokeDasharray=\"2 4\"\n                  />\n                );\n              })}\n            </g>\n\n            <g className=\"font-mono\">\n              {priceTicks.map((tick) => {\n                const y = priceY(tick);\n                if (y < priceY0 - 1 || y > priceY1 + 1) return null;\n                const collides =\n                  (last != null && Math.abs(y - priceY(last.close)) < 13) ||\n                  (hoverY != null && Math.abs(y - hoverY) < 13);\n                if (collides) return null;\n                return (\n                  <text\n                    key={tick}\n                    x={plotX1 + 8}\n                    y={y}\n                    dominantBaseline=\"middle\"\n                    fontSize={10.5}\n                    fill=\"currentColor\"\n                    className=\"tabular-nums\"\n                  >\n                    {formatAxisPrice(tick)}\n                  </text>\n                );\n              })}\n            </g>\n\n            <g className=\"font-mono\">\n              {timeTicks.map((i) => {\n                const candle = view[i];\n                if (!candle) return null;\n                const x = Math.max(plotX0 + 14, Math.min(plotX1 - 14, cx(i)));\n                return (\n                  <text\n                    key={i}\n                    x={x}\n                    y={innerBottom + 13}\n                    textAnchor=\"middle\"\n                    fontSize={10.5}\n                    fill=\"currentColor\"\n                    className=\"tabular-nums\"\n                  >\n                    {DATE_SHORT.format(candle.t)}\n                  </text>\n                );\n              })}\n            </g>\n\n            {hoverIndex != null && view[hoverIndex] ? (\n              <rect\n                x={cx(hoverIndex) - step / 2}\n                y={priceY0}\n                width={step}\n                height={innerBottom - priceY0}\n                fill=\"currentColor\"\n                opacity={0.05}\n              />\n            ) : null}\n\n            {variant === 'area' ? (\n              <g key={introKey}>\n                <path\n                  d={areaPath}\n                  fill={`url(#${uid}-up)`}\n                  clipPath={`url(#${uid}-above)`}\n                  style={\n                    reduce ? undefined : { animation: 'spectrum-mc-fade 620ms ease-out both' }\n                  }\n                />\n                <path\n                  d={areaPath}\n                  fill={`url(#${uid}-down)`}\n                  clipPath={`url(#${uid}-below)`}\n                  style={\n                    reduce ? undefined : { animation: 'spectrum-mc-fade 620ms ease-out both' }\n                  }\n                />\n                <line\n                  x1={plotX0}\n                  x2={plotX1}\n                  y1={baselineY}\n                  y2={baselineY}\n                  stroke=\"currentColor\"\n                  strokeOpacity={0.35}\n                  strokeDasharray=\"3 3\"\n                />\n                <path\n                  d={linePath}\n                  fill=\"none\"\n                  stroke=\"var(--spectrum-chart-up)\"\n                  strokeWidth={2}\n                  strokeLinecap=\"round\"\n                  strokeLinejoin=\"round\"\n                  clipPath={`url(#${uid}-above)`}\n                  pathLength={1}\n                  style={\n                    reduce\n                      ? undefined\n                      : {\n                          strokeDasharray: 1,\n                          animation: 'spectrum-mc-draw 900ms cubic-bezier(0.22, 1, 0.36, 1) both',\n                        }\n                  }\n                />\n                <path\n                  d={linePath}\n                  fill=\"none\"\n                  stroke=\"var(--spectrum-chart-down)\"\n                  strokeWidth={2}\n                  strokeLinecap=\"round\"\n                  strokeLinejoin=\"round\"\n                  clipPath={`url(#${uid}-below)`}\n                  pathLength={1}\n                  style={\n                    reduce\n                      ? undefined\n                      : {\n                          strokeDasharray: 1,\n                          animation: 'spectrum-mc-draw 900ms cubic-bezier(0.22, 1, 0.36, 1) both',\n                        }\n                  }\n                />\n              </g>\n            ) : (\n              <g key={introKey}>\n                {view.map((candle, i) => {\n                  const isUp = candle.close >= candle.open;\n                  const color = isUp ? 'var(--spectrum-chart-up)' : 'var(--spectrum-chart-down)';\n                  const x = cx(i);\n                  const yHigh = priceY(candle.high);\n                  const yLow = priceY(candle.low);\n                  const yOpen = priceY(candle.open);\n                  const yClose = priceY(candle.close);\n                  const top = Math.min(yOpen, yClose);\n                  const bodyH = Math.max(1, Math.abs(yClose - yOpen));\n                  const dim = hoverIndex != null && hoverIndex !== i;\n                  const hollow = hollowUp && isUp && bodyH > 2;\n\n                  return (\n                    <g\n                      key={candle.t}\n                      style={{\n                        opacity: dim ? 0.32 : 1,\n                        transition: reduce ? undefined : 'opacity 160ms ease-out',\n                        transformBox: 'fill-box',\n                        transformOrigin: 'bottom center',\n                        animation: reduce\n                          ? undefined\n                          : `spectrum-mc-rise 420ms cubic-bezier(0.22, 1, 0.36, 1) ${staggerFor(i)}ms both`,\n                      }}\n                    >\n                      <line\n                        x1={x}\n                        x2={x}\n                        y1={yHigh}\n                        y2={yLow}\n                        stroke={color}\n                        strokeWidth={Math.min(1.5, Math.max(1, bodyW * 0.14))}\n                      />\n                      <rect\n                        x={x - bodyW / 2}\n                        y={top}\n                        width={bodyW}\n                        height={bodyH}\n                        rx={Math.min(1.5, bodyW * 0.18)}\n                        fill={hollow ? 'var(--spectrum-chart-surface)' : color}\n                        stroke={color}\n                        strokeWidth={hollow ? 1.25 : 0}\n                      />\n                    </g>\n                  );\n                })}\n              </g>\n            )}\n\n            {showVolume ? (\n              <g key={`${introKey}-vol`}>\n                {view.map((candle, i) => {\n                  const isUp = candle.close >= candle.open;\n                  const barH = Math.max(1, (candle.volume / maxVolume) * volH * 0.88);\n                  const dim = hoverIndex != null && hoverIndex !== i;\n                  return (\n                    <rect\n                      key={candle.t}\n                      x={cx(i) - bodyW / 2}\n                      y={volY1 - barH}\n                      width={bodyW}\n                      height={barH}\n                      rx={Math.min(1.5, bodyW * 0.18)}\n                      fill={isUp ? 'var(--spectrum-chart-up)' : 'var(--spectrum-chart-down)'}\n                      opacity={dim ? 0.12 : 0.3}\n                      style={{\n                        transition: reduce ? undefined : 'opacity 160ms ease-out',\n                        transformBox: 'fill-box',\n                        transformOrigin: 'bottom center',\n                        animation: reduce\n                          ? undefined\n                          : `spectrum-mc-rise 420ms cubic-bezier(0.22, 1, 0.36, 1) ${staggerFor(i)}ms both`,\n                      }}\n                    />\n                  );\n                })}\n                <line\n                  x1={plotX0}\n                  x2={plotX1}\n                  y1={volY0 - VOLUME_GAP / 2}\n                  y2={volY0 - VOLUME_GAP / 2}\n                  stroke=\"currentColor\"\n                  strokeOpacity={0.1}\n                  shapeRendering=\"crispEdges\"\n                />\n              </g>\n            ) : null}\n\n            {last ? (\n              <g>\n                <line\n                  x1={plotX0}\n                  x2={plotX1}\n                  y1={priceY(last.close)}\n                  y2={priceY(last.close)}\n                  stroke={lastColor}\n                  strokeOpacity={0.55}\n                  strokeWidth={1}\n                  strokeDasharray=\"4 4\"\n                />\n                <rect\n                  x={plotX1 + 3}\n                  y={priceY(last.close) - 9}\n                  width={PAD.right - 8}\n                  height={18}\n                  rx={4}\n                  fill={lastColor}\n                />\n                <text\n                  x={plotX1 + 3 + (PAD.right - 8) / 2}\n                  y={priceY(last.close)}\n                  textAnchor=\"middle\"\n                  dominantBaseline=\"middle\"\n                  fontSize={10.5}\n                  fontWeight={600}\n                  fill=\"var(--spectrum-chart-surface)\"\n                  className=\"font-mono tabular-nums\"\n                >\n                  {formatAxisPrice(last.close)}\n                </text>\n                {live && !reduce ? (\n                  <>\n                    <circle\n                      cx={cx(n - 1)}\n                      cy={priceY(last.close)}\n                      r={4}\n                      fill={lastColor}\n                      style={{ animation: 'spectrum-mc-ping 1.6s ease-out infinite' }}\n                    />\n                    <circle cx={cx(n - 1)} cy={priceY(last.close)} r={3} fill={lastColor} />\n                  </>\n                ) : null}\n              </g>\n            ) : null}\n\n            {hoverIndex != null && view[hoverIndex] ? (\n              <g\n                style={{\n                  transition: reduce ? undefined : 'transform 110ms cubic-bezier(0.22, 1, 0.36, 1)',\n                }}\n              >\n                <line\n                  x1={cx(hoverIndex)}\n                  x2={cx(hoverIndex)}\n                  y1={priceY0}\n                  y2={innerBottom}\n                  stroke=\"currentColor\"\n                  strokeOpacity={0.45}\n                  strokeDasharray=\"3 3\"\n                />\n                {hoverY != null ? (\n                  <>\n                    <line\n                      x1={plotX0}\n                      x2={plotX1}\n                      y1={hoverY}\n                      y2={hoverY}\n                      stroke=\"currentColor\"\n                      strokeOpacity={0.45}\n                      strokeDasharray=\"3 3\"\n                    />\n                    <rect\n                      x={plotX1 + 3}\n                      y={hoverY - 9}\n                      width={PAD.right - 8}\n                      height={18}\n                      rx={4}\n                      className=\"fill-neutral-900 dark:fill-white\"\n                    />\n                    <text\n                      x={plotX1 + 3 + (PAD.right - 8) / 2}\n                      y={hoverY}\n                      textAnchor=\"middle\"\n                      dominantBaseline=\"middle\"\n                      fontSize={10.5}\n                      fontWeight={600}\n                      className=\"fill-white font-mono tabular-nums dark:fill-neutral-950\"\n                    >\n                      {formatAxisPrice(priceAt(hoverY))}\n                    </text>\n                  </>\n                ) : null}\n\n                <g\n                  transform={`translate(${Math.max(\n                    plotX0 + 28,\n                    Math.min(plotX1 - 28, cx(hoverIndex)),\n                  )}, ${innerBottom + 4})`}\n                >\n                  <rect\n                    x={-27}\n                    y={0}\n                    width={54}\n                    height={16}\n                    rx={4}\n                    className=\"fill-neutral-900 dark:fill-white\"\n                  />\n                  <text\n                    x={0}\n                    y={8.5}\n                    textAnchor=\"middle\"\n                    dominantBaseline=\"middle\"\n                    fontSize={10}\n                    fontWeight={600}\n                    className=\"fill-white font-mono tabular-nums dark:fill-neutral-950\"\n                  >\n                    {DATE_SHORT.format(view[hoverIndex].t)}\n                  </text>\n                </g>\n\n                <circle\n                  cx={cx(hoverIndex)}\n                  cy={priceY(view[hoverIndex].close)}\n                  r={4.5}\n                  fill=\"var(--spectrum-chart-surface)\"\n                  stroke={view[hoverIndex].close >= view[hoverIndex].open\n                    ? 'var(--spectrum-chart-up)'\n                    : 'var(--spectrum-chart-down)'}\n                  strokeWidth={2}\n                />\n              </g>\n            ) : null}\n          </svg>\n        )}\n      </div>\n      </ChartState>\n    </div>\n  );\n}\n\nexport function DefaultMarketChart(props: MarketChartProps) {\n  return <MarketChart {...props} />;\n}\n\nexport function StockMarketChart(props: MarketChartProps) {\n  return <MarketChart data={AAPL_MARKET} symbol=\"AAPL\" name=\"Apple Inc.\" {...props} />;\n}\n\nexport function BitcoinMarketChart(props: MarketChartProps) {\n  return (\n    <MarketChart data={BTC_MARKET} symbol=\"BTC\" name=\"Bitcoin\" compactPrice hollowUp {...props} />\n  );\n}\n\nexport function AreaMarketChart(props: MarketChartProps) {\n  return <MarketChart variant=\"area\" showVolume={false} defaultRange=\"1Y\" {...props} />;\n}\n\nexport function LiveMarketChart(props: MarketChartProps) {\n  return <MarketChart live defaultRange=\"1M\" {...props} />;\n}\n\nexport function CompactMarketChart(props: MarketChartProps) {\n  return (\n    <MarketChart showVolume={false} showRangeSelector={false} height={220} defaultRange=\"3M\" {...props} />\n  );\n}\n\n",
      "type": "registry:component",
      "target": "components/spectrumui/charts/market-chart.tsx"
    },
    {
      "path": "app/registry/charts/chart-data.tsx",
      "content": "'use client';\n\nimport * as React from 'react';\nimport { cn } from '@/lib/utils';\nimport {\n  type Candle,\n  type ChartStatus,\n  DAY_MS,\n  SOL_MARKET,\n  mulberry32,\n  round2,\n  seriesVarsClassName,\n} from './chart-engine';\nimport { MarketChart } from './market-chart';\n\ntype Outcome = 'ok' | 'empty' | 'fail';\n\nfunction fakeFetch(outcome: Outcome, signal?: AbortSignal): Promise<Candle[]> {\n  return new Promise((resolve, reject) => {\n    const id = setTimeout(() => {\n      if (outcome === 'fail') reject(new Error('503 from /api/candles'));\n      else resolve(outcome === 'empty' ? [] : SOL_MARKET.slice(-120));\n    }, 1100);\n    signal?.addEventListener('abort', () => {\n      clearTimeout(id);\n      reject(new DOMException('Aborted', 'AbortError'));\n    });\n  });\n}\n\nexport function useChartData(outcome: Outcome) {\n  const [nonce, setNonce] = React.useState(0);\n  const requestKey = `${outcome}:${nonce}`;\n  const [result, setResult] = React.useState<{\n    key: string;\n    data: Candle[];\n    status: ChartStatus;\n  } | null>(null);\n\n  React.useEffect(() => {\n    const controller = new AbortController();\n    let live = true;\n\n    fakeFetch(outcome, controller.signal)\n      .then((rows) => {\n        if (!live) return;\n        setResult({ key: requestKey, data: rows, status: rows.length ? 'ready' : 'empty' });\n      })\n      .catch((error: unknown) => {\n        if (!live || (error as Error).name === 'AbortError') return;\n        setResult({ key: requestKey, data: [], status: 'error' });\n      });\n\n    return () => {\n      live = false;\n      controller.abort();\n    };\n  }, [outcome, requestKey]);\n\n  const fresh = result?.key === requestKey ? result : null;\n\n  return {\n    data: fresh?.data ?? [],\n    status: fresh?.status ?? ('loading' as ChartStatus),\n    reload: React.useCallback(() => setNonce((n) => n + 1), []),\n  };\n}\n\nfunction OutcomePicker({\n  value,\n  onChange,\n}: {\n  value: Outcome;\n  onChange: (next: Outcome) => void;\n}) {\n  return (\n    <div className=\"mb-3 inline-flex items-center gap-0.5 rounded-full bg-black/[0.045] p-0.5 dark:bg-white/[0.07]\">\n      {(['ok', 'empty', 'fail'] as Outcome[]).map((option) => (\n        <button\n          key={option}\n          type=\"button\"\n          onClick={() => onChange(option)}\n          aria-pressed={value === option}\n          className={cn(\n            'rounded-full px-3 py-1 font-mono text-[11px] leading-none transition-colors',\n            'focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-black/20 dark:focus-visible:ring-white/25',\n            value === option\n              ? 'bg-white text-neutral-950 shadow-sm dark:bg-white/12 dark:text-white'\n              : 'text-neutral-500 hover:text-neutral-800 dark:text-neutral-400 dark:hover:text-neutral-200',\n          )}\n        >\n          server returns {option}\n        </button>\n      ))}\n    </div>\n  );\n}\n\nexport function FetchRecipe({ className }: { className?: string }) {\n  const [outcome, setOutcome] = React.useState<Outcome>('ok');\n  const { data, status, reload } = useChartData(outcome);\n\n  return (\n    <div className={cn('w-full', seriesVarsClassName, className)}>\n      <OutcomePicker value={outcome} onChange={setOutcome} />\n      <MarketChart\n        data={data}\n        status={status}\n        onRetry={reload}\n        symbol=\"SOL\"\n        name=\"from /api/candles\"\n        height={320}\n        defaultRange=\"3M\"\n      />\n    </div>\n  );\n}\n\nexport function toChartStatus({\n  isLoading,\n  error,\n  rows,\n}: {\n  isLoading: boolean;\n  error?: unknown;\n  rows?: readonly unknown[] | null;\n}): ChartStatus {\n  if (isLoading) return 'loading';\n  if (error) return 'error';\n  if (!rows || rows.length === 0) return 'empty';\n  return 'ready';\n}\n\nexport function QueryRecipe({ className }: { className?: string }) {\n  const [isLoading, setLoading] = React.useState(true);\n  React.useEffect(() => {\n    const id = window.setTimeout(() => setLoading(false), 1200);\n    return () => window.clearTimeout(id);\n  }, []);\n\n  const rows = isLoading ? [] : SOL_MARKET.slice(-90);\n  const status = toChartStatus({ isLoading, rows });\n\n  return (\n    <div className={cn('w-full', seriesVarsClassName, className)}>\n      <MarketChart\n        data={rows}\n        status={status}\n        symbol=\"SOL\"\n        name=\"via useSWR\"\n        height={320}\n        defaultRange=\"3M\"\n        showRangeSelector={false}\n      />\n    </div>\n  );\n}\n\nexport function StreamRecipe({ className }: { className?: string }) {\n  const [data, setData] = React.useState<Candle[]>(() => SOL_MARKET.slice(-60));\n\n  React.useEffect(() => {\n    const rand = mulberry32(0xfeed);\n    const id = window.setInterval(() => {\n      setData((prev) => {\n        const last = prev[prev.length - 1];\n        const close = round2(Math.max(0.01, last.close * (1 + (rand() - 0.47) * 0.02)));\n        const next: Candle = {\n          t: last.t + DAY_MS,\n          open: last.close,\n          high: round2(Math.max(last.close, close) * (1 + rand() * 0.005)),\n          low: round2(Math.min(last.close, close) * (1 - rand() * 0.005)),\n          close,\n          volume: Math.round((0.6 + rand()) * 1_000_000),\n        };\n        return [...prev, next].slice(-90);\n      });\n    }, 1500);\n    return () => window.clearInterval(id);\n  }, []);\n\n  return (\n    <div className={cn('w-full', seriesVarsClassName, className)}>\n      <MarketChart\n        data={data}\n        symbol=\"SOL\"\n        name=\"streaming\"\n        height={320}\n        defaultRange=\"1M\"\n        showRangeSelector={false}\n      />\n    </div>\n  );\n}\n\nexport function DefaultChartData(props: { className?: string }) {\n  return <FetchRecipe {...props} />;\n}\n",
      "type": "registry:component",
      "target": "components/spectrumui/charts/chart-data.tsx"
    }
  ],
  "type": "registry:component"
}
