{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "trades-feed",
  "type": "registry:ui",
  "title": "Trades Feed",
  "description": "Live time-and-sales tape with side colors and new-print flash.",
  "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/trades-feed.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 TradePrint = { id: number; t: number; price: number; amount: number; side: \"buy\" | \"sell\" };\n\nfunction nextPrint(id: number, previous: number, rand: () => number): TradePrint {\n  const side = rand() > 0.48 ? \"buy\" : \"sell\";\n  const drift = (rand() - 0.5) * previous * 0.0012;\n  return {\n    id,\n    t: Date.now(),\n    price: Math.max(previous * 0.5, previous + drift),\n    amount: 0.001 + rand() * rand() * 3.5,\n    side,\n  };\n}\n\nexport function TradesFeed({\n  symbol = \"BTC/USDT\",\n  basePrice = 97500,\n  seed = 41,\n  tickMs = 900,\n  rows = 16,\n  className,\n  isLoading,\n  reaction,\n}: {\n  symbol?: string;\n  basePrice?: number;\n  seed?: number;\n  tickMs?: number;\n  rows?: number;\n  className?: string;\n  isLoading?: boolean;\n  reaction?: ChartReactionOptions;\n}) {\n  const randRef = React.useRef<() => number>(mulberry32(seed));\n  const idRef = React.useRef(1);\n  const [prints, setPrints] = React.useState<TradePrint[]>([]);\n\n  React.useEffect(() => {\n    randRef.current = mulberry32(seed);\n    idRef.current = 1;\n    const out: TradePrint[] = [];\n    let price = basePrice;\n    const now = Date.now();\n    for (let index = 0; index < rows; index++) {\n      const print = nextPrint(idRef.current++, price, randRef.current);\n      print.t = now - (rows - index) * tickMs;\n      price = print.price;\n      out.push(print);\n    }\n    setPrints([...out].reverse());\n    const id = window.setInterval(() => {\n      if (document.hidden) return;\n      setPrints((current) => {\n        const currentPrice = current[0]?.price ?? basePrice;\n        return [nextPrint(idRef.current++, currentPrice, randRef.current), ...current].slice(0, rows);\n      });\n    }, tickMs);\n    return () => window.clearInterval(id);\n  }, [seed, tickMs, rows, basePrice]);\n\n  void reaction;\n\n  return (\n    <Card className={cn(\"min-w-0 w-full p-5\", className)} role=\"region\" aria-label={`${symbol} latest trades`}>\n      <ChartSkeleton isLoading={isLoading}>\n        <style>{`@keyframes trade-in { from { background-color: var(--accent); } to { background-color: transparent; } }`}</style>\n        <div className=\"flex items-center justify-between gap-2\">\n          <p className=\"truncate text-[15px] font-medium tracking-tight\">Latest trades</p>\n          {!isLoading ? <ChartReaction reaction={reaction} /> : <p className=\"font-mono text-[11px] text-muted-foreground\">{symbol}</p>}\n        </div>\n        <div className=\"mt-2 grid grid-cols-[1fr_1fr_1fr] px-1 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\">Time</span>\n        </div>\n        <ul className=\"mt-1 space-y-[1px]\" aria-live=\"off\" aria-label=\"Recent prints\">\n          {prints.map((print, index) => (\n            <li\n              key={print.id}\n              style={index === 0 ? { animation: \"trade-in 900ms ease-out\" } : undefined}\n              className=\"grid grid-cols-[1fr_1fr_1fr] rounded px-1 py-[3px] font-mono text-[11px] tabular-nums\"\n            >\n              <span className={print.side === \"buy\" ? \"text-chart-up\" : \"text-destructive\"}>${formatPrice(print.price)}</span>\n              <span className=\"text-right text-foreground\">{print.amount.toFixed(4)}</span>\n              <span className=\"text-right text-muted-foreground\">{new Date(print.t).toLocaleTimeString(\"en-US\", { hour12: false })}</span>\n            </li>\n          ))}\n        </ul>\n      </ChartSkeleton>\n    </Card>\n  );\n}\n"
    }
  ]
}