{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "order-book",
  "title": "Order Book",
  "description": "A trading ladder with cumulative depth bars, a spread row, and flashing updates in live mode.",
  "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/order-book.tsx",
      "content": "'use client';\n\nimport * as React from 'react';\nimport { cn } from '@/lib/utils';\nimport {\n  type ChartStatus,\n  ChartState,\n  type BookLevel,\n  DOWN,\n  EASE,\n  Keyframes,\n  type OrderBook,\n  UP,\n  formatMoney,\n  generateOrderBook,\n  marketVarsClassName,\n  mulberry32,\n  usePrefersReducedMotion,\n} from './chart-engine';\n\nexport const SOL_LADDER = generateOrderBook({ mid: 245.85, seed: 90_210, levels: 9, depth: 820 });\nexport const ETH_LADDER = generateOrderBook({\n  mid: 3_842.6,\n  seed: 55_318,\n  levels: 9,\n  tick: 0.4,\n  depth: 140,\n});\n\nfunction formatSpread(spread: number, mid: number) {\n  const digits = mid >= 100 ? 2 : mid >= 1 ? 3 : 5;\n  return new Intl.NumberFormat('en-US', {\n    style: 'currency',\n    currency: 'USD',\n    minimumFractionDigits: digits,\n    maximumFractionDigits: digits,\n  }).format(spread);\n}\n\nfunction compactSize(value: number) {\n  return new Intl.NumberFormat('en-US', {\n    notation: 'compact',\n    maximumFractionDigits: value >= 100 ? 1 : 2,\n  }).format(value);\n}\n\ntype Row = BookLevel & { side: 'bid' | 'ask'; index: number };\n\nfunction BookRow({\n  row,\n  maxTotal,\n  highlighted,\n  flashed,\n  flashKey,\n  reduce,\n  onEnter,\n}: {\n  row: Row;\n  maxTotal: number;\n  highlighted: boolean;\n  flashed: boolean;\n  flashKey: number;\n  reduce: boolean;\n  onEnter: () => void;\n}) {\n  const color = row.side === 'bid' ? UP : DOWN;\n  const depth = Math.max(0.02, row.total / maxTotal);\n\n  return (\n    <li\n      onPointerEnter={onEnter}\n      className=\"relative flex h-[22px] cursor-default items-center px-2 font-mono text-[11px] tabular-nums\"\n    >\n      <span\n        aria-hidden\n        className=\"absolute inset-y-px right-1 rounded-[3px]\"\n        style={{\n          width: `calc(${(depth * 100).toFixed(2)}% - 4px)`,\n          background: color,\n          opacity: highlighted ? 0.28 : 0.13,\n          transition: reduce ? undefined : `opacity 140ms ease-out, width 320ms ${EASE}`,\n        }}\n      />\n      {flashed && !reduce ? (\n        <span\n          key={flashKey}\n          aria-hidden\n          className=\"absolute inset-y-0 left-0 right-0\"\n          style={{ background: color, animation: 'spectrum-mc-flash 520ms ease-out forwards' }}\n        />\n      ) : null}\n      <span className=\"relative z-10 w-[38%] text-left\" style={{ color }}>\n        {formatMoney(row.price)}\n      </span>\n      <span className=\"relative z-10 w-[31%] text-right text-neutral-700 dark:text-neutral-300\">\n        {compactSize(row.size)}\n      </span>\n      <span className=\"relative z-10 w-[31%] text-right text-neutral-400 dark:text-neutral-500\">\n        {compactSize(row.total)}\n      </span>\n    </li>\n  );\n}\n\nexport interface OrderBookProps {\n  className?: string;\n  book?: OrderBook;\n  symbol?: string;\n  live?: boolean;\n  status?: ChartStatus;\n  onRetry?: () => void;\n}\n\nexport function OrderBookLadder({\n  className,\n  book = SOL_LADDER,\n  symbol = 'SOL',\n  live = false,\n  status = 'ready',\n  onRetry,\n}: OrderBookProps) {\n  const reduce = usePrefersReducedMotion();\n  const [hovered, setHovered] = React.useState<{ side: 'bid' | 'ask'; index: number } | null>(null);\n  const [nonce, setNonce] = React.useState(0);\n\n  const current = React.useMemo(() => {\n    if (!live || nonce === 0) return book;\n    const rand = mulberry32(0xb00c + nonce);\n    const jitter = (levels: BookLevel[]) => {\n      let total = 0;\n      return levels.map((level) => {\n        const size = Math.max(1, Math.round(level.size * (0.82 + rand() * 0.4)));\n        total += size;\n        return { ...level, size, total };\n      });\n    };\n    return { ...book, bids: jitter(book.bids), asks: jitter(book.asks) };\n  }, [book, live, nonce]);\n\n  React.useEffect(() => {\n    if (!live) return;\n    const id = window.setInterval(() => setNonce((n) => n + 1), 1400);\n    return () => window.clearInterval(id);\n  }, [live]);\n\n  const flashed = React.useMemo(() => {\n    const next = new Set<string>();\n    if (!live || nonce === 0) return next;\n    const rand = mulberry32(0xf1a5 + nonce);\n    for (const side of ['bid', 'ask'] as const) {\n      const levels = side === 'bid' ? current.bids : current.asks;\n      for (let i = 0; i < levels.length; i += 1) {\n        if (rand() > 0.62) next.add(`${side}-${i}`);\n      }\n    }\n    return next;\n  }, [live, nonce, current]);\n\n  const maxTotal = Math.max(\n    current.bids[current.bids.length - 1]?.total ?? 1,\n    current.asks[current.asks.length - 1]?.total ?? 1,\n    1,\n  );\n\n  const bestBid = current.bids[0]?.price ?? current.mid;\n  const bestAsk = current.asks[0]?.price ?? current.mid;\n  const spread = bestAsk - bestBid;\n\n  const askRows: Row[] = current.asks\n    .map((level, index) => ({ ...level, side: 'ask' as const, index }))\n    .reverse();\n  const bidRows: Row[] = current.bids.map((level, index) => ({\n    ...level,\n    side: 'bid' as const,\n    index,\n  }));\n\n  const isHighlighted = (row: Row) =>\n    hovered != null && hovered.side === row.side && row.index <= hovered.index;\n\n  return (\n    <div\n      className={cn(\n        'w-full overflow-hidden rounded-xl border border-black/8 dark:border-white/10',\n        marketVarsClassName,\n        className,\n      )}\n      onPointerLeave={() => setHovered(null)}\n    >\n      <Keyframes />\n\n      <div className=\"flex items-center justify-between border-b border-black/6 px-2.5 py-2 dark:border-white/8\">\n        <span className=\"font-mono text-[11px] font-medium tracking-wide text-neutral-950 dark:text-white\">\n          {symbol} <span className=\"text-neutral-400\">Order book</span>\n        </span>\n        {live ? (\n          <span className=\"inline-flex items-center gap-1 font-mono text-[9px] uppercase tracking-widest text-neutral-500 dark:text-neutral-400\">\n            <span\n              className=\"size-1.5 rounded-full\"\n              style={{\n                background: 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      <ChartState\n        status={status}\n        height={320}\n        variant=\"rows\"\n        empty={{ title: 'Book is empty', description: 'No resting orders on this pair right now.' }}\n        onRetry={onRetry}\n      >\n      <div className=\"flex px-2 py-1.5 font-mono text-[9.5px] uppercase tracking-wider text-neutral-400 dark:text-neutral-500\">\n        <span className=\"w-[38%]\">Price</span>\n        <span className=\"w-[31%] text-right\">Size</span>\n        <span className=\"w-[31%] text-right\">Total</span>\n      </div>\n\n      <ul>\n        {askRows.map((row) => (\n          <BookRow\n            key={`ask-${row.index}`}\n            row={row}\n            maxTotal={maxTotal}\n            highlighted={isHighlighted(row)}\n            flashed={flashed.has(`ask-${row.index}`)}\n            flashKey={nonce}\n            reduce={reduce}\n            onEnter={() => setHovered({ side: 'ask', index: row.index })}\n          />\n        ))}\n      </ul>\n\n      <div className=\"my-1 flex items-center justify-between border-y border-black/6 bg-black/[0.02] px-2 py-1.5 font-mono text-[11px] tabular-nums dark:border-white/8 dark:bg-white/[0.03]\">\n        <span className=\"font-medium text-neutral-950 dark:text-white\">\n          {formatMoney(current.mid)}\n        </span>\n        <span className=\"text-[10px] text-neutral-400 dark:text-neutral-500\">\n          spread {formatSpread(spread, current.mid)} · {((spread / current.mid) * 100).toFixed(3)}%\n        </span>\n      </div>\n\n      <ul className=\"pb-1\">\n        {bidRows.map((row) => (\n          <BookRow\n            key={`bid-${row.index}`}\n            row={row}\n            maxTotal={maxTotal}\n            highlighted={isHighlighted(row)}\n            flashed={flashed.has(`bid-${row.index}`)}\n            flashKey={nonce}\n            reduce={reduce}\n            onEnter={() => setHovered({ side: 'bid', index: row.index })}\n          />\n        ))}\n      </ul>\n      </ChartState>\n    </div>\n  );\n}\n\nexport function DefaultOrderBook(props: OrderBookProps) {\n  return <OrderBookLadder {...props} />;\n}\n\nexport function LiveOrderBook(props: OrderBookProps) {\n  return <OrderBookLadder live {...props} />;\n}\n\nexport function EthereumOrderBook(props: OrderBookProps) {\n  return <OrderBookLadder book={ETH_LADDER} symbol=\"ETH\" {...props} />;\n}\n",
      "type": "registry:component",
      "target": "components/spectrumui/charts/order-book.tsx"
    }
  ],
  "type": "registry:component"
}
