{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "order-book",
  "type": "registry:ui",
  "title": "Order Book",
  "description": "Live bid/ask ladder with depth bars, spread readout, and imbalance meter.",
  "registryDependencies": [
    "https://livedocs.xyz/r/chart.json",
    "https://livedocs.xyz/r/chart-reactions.json",
    "https://livedocs.xyz/r/card.json",
    "https://livedocs.xyz/r/utils.json"
  ],
  "files": [
    {
      "path": "components/ui/crypto-feed.ts",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nexport type { ChartReactionOptions } from \"@/components/ui/chart-reactions\";\n\nexport type CryptoTick = { t: number; price: number; volume: number };\n\nexport function mulberry32(seed: number) {\n  let state = seed >>> 0;\n  return () => {\n    state |= 0;\n    state = (state + 0x6d2b79f5) | 0;\n    let word = Math.imul(state ^ (state >>> 15), 1 | state);\n    word = (word + Math.imul(word ^ (word >>> 7), 61 | word)) ^ word;\n    return ((word ^ (word >>> 14)) >>> 0) / 4294967296;\n  };\n}\n\nexport function genTicks(seed: number, points: number, start: number, drift: number, vol: number, stepMs = 60000): CryptoTick[] {\n  const rand = mulberry32(seed);\n  const now = Math.floor(Date.now() / 60000) * 60000;\n  let price = start;\n  return Array.from({ length: points }, (_, index) => {\n    price = Math.max(start * 0.2, price * (1 + drift + (rand() - 0.5) * vol));\n    return { t: now - (points - 1 - index) * stepMs, price, volume: Math.round(20 + rand() * 180) };\n  });\n}\n\nexport function nextTick(previous: CryptoTick, seedRef: { rand: () => number }, drift: number, vol: number, stepMs = 60000): CryptoTick {\n  const price = Math.max(previous.price * 0.2, previous.price * (1 + drift + (seedRef.rand() - 0.5) * vol));\n  return { t: previous.t + stepMs, price, volume: Math.round(20 + seedRef.rand() * 180) };\n}\n\nexport function movingAverage(values: number[], window: number): (number | null)[] {\n  return values.map((_, index) => {\n    if (index < window - 1) return null;\n    let sum = 0;\n    for (let offset = 0; offset < window; offset++) sum += values[index - offset]!;\n    return sum / window;\n  });\n}\n\nexport function formatPrice(value: number) {\n  const digits = value >= 1000 ? 2 : value >= 10 ? 2 : 4;\n  return value.toLocaleString(\"en-US\", { minimumFractionDigits: digits, maximumFractionDigits: digits });\n}\n\nexport function formatCompactUsd(value: number) {\n  if (Math.abs(value) >= 1_000_000_000) return `$${(value / 1_000_000_000).toFixed(2)}B`;\n  if (Math.abs(value) >= 1_000_000) return `$${(value / 1_000_000).toFixed(2)}M`;\n  if (Math.abs(value) >= 1_000) return `$${(value / 1_000).toFixed(2)}K`;\n  return `$${value.toFixed(2)}`;\n}\n\nexport function useLiveTicks({\n  seed = 7,\n  points = 120,\n  start = 100,\n  drift = 0.0004,\n  vol = 0.006,\n  intervalMs = 1200,\n  live = true,\n}: {\n  seed?: number;\n  points?: number;\n  start?: number;\n  drift?: number;\n  vol?: number;\n  intervalMs?: number;\n  live?: boolean;\n} = {}) {\n  const randRef = React.useRef<() => number>(mulberry32(seed ^ 0x9e3779b9));\n  const [ticks, setTicks] = React.useState<CryptoTick[]>(() => genTicks(seed, points, start, drift, vol));\n  const [streaming, setStreaming] = React.useState(live);\n\n  React.useEffect(() => {\n    setTicks(genTicks(seed, points, start, drift, vol));\n    randRef.current = mulberry32(seed ^ 0x9e3779b9);\n  }, [seed, points, start, drift, vol]);\n\n  React.useEffect(() => {\n    if (!live || !streaming) return;\n    if (typeof document !== \"undefined\" && document.hidden) return;\n    const id = window.setInterval(() => {\n      setTicks((current) => {\n        if (!current.length) return current;\n        const next = nextTick(current[current.length - 1]!, { rand: randRef.current }, drift, vol);\n        return [...current.slice(-points + 1), next];\n      });\n    }, intervalMs);\n    return () => window.clearInterval(id);\n  }, [live, streaming, intervalMs, points, drift, vol]);\n\n  const last = ticks[ticks.length - 1];\n  const first = ticks[0];\n  const change = last && first ? (last.price / first.price - 1) * 100 : 0;\n  return { ticks, last, change, streaming, setStreaming };\n}\n"
    },
    {
      "path": "components/ui/order-book.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { Card } from \"@/components/ui/card\";\nimport { ChartReaction, ChartSkeleton } from \"@/components/ui/chart-reactions\";\nimport { formatPrice, mulberry32, type ChartReactionOptions } from \"@/components/ui/crypto-feed\";\nimport { cn } from \"@/lib/utils\";\n\nexport type BookLevel = { price: number; amount: number; total: number };\n\nfunction buildBook(seed: number, mid: number, rows: number, tick: number): { asks: BookLevel[]; bids: BookLevel[] } {\n  const rand = mulberry32(seed);\n  const asks: BookLevel[] = [];\n  const bids: BookLevel[] = [];\n  let askTotal = 0;\n  let bidTotal = 0;\n  for (let index = 1; index <= rows; index++) {\n    const askAmount = 0.05 + rand() * 2.4 + (index === 3 ? 4.5 : 0);\n    const bidAmount = 0.05 + rand() * 2.4 + (index === 2 ? 5.2 : 0);\n    askTotal += askAmount;\n    bidTotal += bidAmount;\n    asks.push({ price: mid + index * tick, amount: askAmount, total: askTotal });\n    bids.push({ price: mid - index * tick, amount: bidAmount, total: bidTotal });\n  }\n  return { asks: [...asks].reverse(), bids };\n}\n\nfunction Row({ level, max, side, index }: { level: BookLevel; max: number; side: \"bid\" | \"ask\"; index: number }) {\n  return (\n    <div\n      role=\"row\"\n      aria-label={`${side === \"bid\" ? \"Bid\" : \"Ask\"} ${level.price} amount ${level.amount.toFixed(4)}`}\n      className=\"relative grid grid-cols-[1fr_1fr_1fr] px-2 py-[3px] font-mono text-[11px] tabular-nums\"\n    >\n      <span\n        aria-hidden\n        className={cn(\"absolute inset-y-[1px] right-0 rounded-sm\", side === \"bid\" ? \"bg-chart-up/15\" : \"bg-destructive/15\")}\n        style={{ width: `${Math.max(2, (level.total / max) * 100)}%` }}\n      />\n      <span className={cn(\"relative\", side === \"bid\" ? \"text-chart-up\" : \"text-destructive\")}>${formatPrice(level.price)}</span>\n      <span className=\"relative text-right text-foreground\">{level.amount.toFixed(4)}</span>\n      <span className=\"relative text-right text-muted-foreground\">{level.total.toFixed(4)}</span>\n    </div>\n  );\n}\n\nexport function OrderBook({\n  symbol = \"BTC/USDT\",\n  basePrice = 97500,\n  tickSize = 10,\n  rows = 9,\n  seed = 21,\n  tickMs = 1500,\n  className,\n  isLoading,\n  reaction,\n}: {\n  symbol?: string;\n  basePrice?: number;\n  tickSize?: number;\n  rows?: number;\n  seed?: number;\n  tickMs?: number;\n  className?: string;\n  isLoading?: boolean;\n  reaction?: ChartReactionOptions;\n}) {\n  const [nonce, setNonce] = React.useState(0);\n  const book = React.useMemo(() => buildBook(seed + nonce * 101, basePrice, rows, tickSize), [seed, nonce, basePrice, rows, tickSize]);\n  const drift = React.useRef(mulberry32(seed));\n\n  React.useEffect(() => {\n    const id = window.setInterval(() => {\n      if (document.hidden) return;\n      drift.current();\n      setNonce((value) => value + 1);\n    }, tickMs);\n    return () => window.clearInterval(id);\n  }, [tickMs]);\n\n  const bestAsk = book.asks[book.asks.length - 1]!;\n  const bestBid = book.bids[0]!;\n  const spread = bestAsk.price - bestBid.price;\n  const max = Math.max(...book.asks.map((level) => level.total), ...book.bids.map((level) => level.total));\n  const bidVolume = book.bids.reduce((sum, level) => sum + level.amount, 0);\n  const askVolume = book.asks.reduce((sum, level) => sum + level.amount, 0);\n  const bidShare = (bidVolume / (bidVolume + askVolume)) * 100;\n\n  void reaction;\n\n  return (\n    <Card className={cn(\"min-w-0 w-full p-5\", className)} role=\"region\" aria-label={`${symbol} order book`}>\n      <ChartSkeleton isLoading={isLoading}>\n        <div className=\"flex items-center justify-between gap-2\">\n          <p className=\"truncate text-[15px] font-medium tracking-tight\">Order book</p>\n          {!isLoading ? <ChartReaction reaction={reaction} /> : <p className=\"font-mono text-[11px] text-muted-foreground\">{symbol}</p>}\n        </div>\n        <div className=\"mt-1 grid grid-cols-[1fr_1fr_1fr] px-2 font-mono text-[10px] uppercase tracking-wide text-muted-foreground\" aria-hidden>\n          <span>Price</span><span className=\"text-right\">Amount</span><span className=\"text-right\">Total</span>\n        </div>\n        <div role=\"table\" aria-label=\"Asks\" className=\"mt-1\">\n          {book.asks.map((level, index) => (\n            <Row key={`ask-${level.price}`} level={level} max={max} side=\"ask\" index={index} />\n          ))}\n        </div>\n        <div className=\"flex items-center gap-2 border-y border-border px-2 py-1.5\" role=\"status\">\n          <p className=\"font-mono text-sm font-semibold tabular-nums text-foreground\">${formatPrice((bestAsk.price + bestBid.price) / 2)}</p>\n          <p className=\"font-mono text-[11px] tabular-nums text-muted-foreground\">Spread ${formatPrice(spread)}</p>\n        </div>\n        <div role=\"table\" aria-label=\"Bids\">\n          {book.bids.map((level, index) => (\n            <Row key={`bid-${level.price}`} level={level} max={max} side=\"bid\" index={index} />\n          ))}\n        </div>\n        <div className=\"mt-3\">\n          <div className=\"flex h-1.5 overflow-hidden rounded-full bg-destructive/25\" role=\"img\" aria-label={`Bid ${bidShare.toFixed(0)} percent, ask ${(100 - bidShare).toFixed(0)} percent`}>\n            <span className=\"h-full bg-chart-up\" style={{ width: `${bidShare}%` }} />\n          </div>\n          <div className=\"mt-1 flex justify-between font-mono text-[10px] tabular-nums text-muted-foreground\">\n            <span className=\"text-chart-up\">B {bidShare.toFixed(1)}%</span>\n            <span className=\"text-destructive\">A {(100 - bidShare).toFixed(1)}%</span>\n          </div>\n        </div>\n      </ChartSkeleton>\n    </Card>\n  );\n}\n"
    }
  ]
}