{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "eased-realtime-core",
  "title": "Realtime core primitive",
  "description": "Deterministic live data: a seeded RNG and tick stream (prices, rates, vitals), useClock and useTicker that pause off-screen and in hidden tabs and slow under reduced motion, flash-on-change state, OHLC candles on chart-core scales, and an order-book depth model with a synthetic churning book.",
  "dependencies": [
    "motion",
    "d3-scale"
  ],
  "registryDependencies": [
    "https://eased.dev/r/eased-tokens.json",
    "https://eased.dev/r/eased-motion.json",
    "https://eased.dev/r/eased-chart-core.json"
  ],
  "files": [
    {
      "path": "src/registry/eased/_primitives/realtime-core/candles.ts",
      "content": "import { bands, linear, niceScale, type Domain } from \"../chart-core/scales\";\nimport type { TickStream } from \"./stream\";\n\n/** One OHLC bar. `t` is the bucket's start, epoch ms. */\nexport type Candle = { t: number; open: number; high: number; low: number; close: number; volume?: number };\n\n/** Buckets timestamped prices into candles (`bucketMs` wide, aligned to multiples of it). */\nexport function toCandles(points: readonly { t: number; value: number; volume?: number }[], bucketMs: number): Candle[] {\n  const out: Candle[] = [];\n  for (const p of points) {\n    const t = Math.floor(p.t / bucketMs) * bucketMs;\n    const last = out[out.length - 1];\n    if (last && last.t === t) {\n      last.high = Math.max(last.high, p.value);\n      last.low = Math.min(last.low, p.value);\n      last.close = p.value;\n      if (p.volume !== undefined) last.volume = (last.volume ?? 0) + p.volume;\n    } else {\n      out.push({ t, open: last?.close ?? p.value, high: p.value, low: p.value, close: p.value, volume: p.volume });\n      // A gap-free chart: the new candle opens at the previous close and spans it.\n      const c = out[out.length - 1];\n      c.high = Math.max(c.high, c.open);\n      c.low = Math.min(c.low, c.open);\n    }\n  }\n  return out;\n}\n\n/**\n * `count` candles from one stream series, each built from `ticksPerCandle` ticks and ending at\n * tick `end`. Deterministic, so the same chart renders on the server and the client.\n */\nexport function candlesFromStream(stream: TickStream, id: string, { end, count, ticksPerCandle = 12 }: { end: number; count: number; ticksPerCandle?: number }): Candle[] {\n  const first = Math.max(0, end - count * ticksPerCandle + 1);\n  const ticks = stream.window(end, end - first + 1);\n  const bucketMs = stream.intervalMs * ticksPerCandle;\n  return toCandles(\n    ticks.map((t) => ({ t: t.t, value: t.values[id] })),\n    bucketMs,\n  ).slice(-count);\n}\n\n/**\n * Folds a live price into the candles: it updates the last candle while `t` is inside its\n * bucket and opens a new one when it isn't, keeping at most `max`. Immutable.\n */\nexport function pushPrice(candles: readonly Candle[], price: number, t: number, bucketMs: number, max = Infinity): Candle[] {\n  const bucket = Math.floor(t / bucketMs) * bucketMs;\n  const last = candles[candles.length - 1];\n  if (last && last.t === bucket) {\n    const next = { ...last, high: Math.max(last.high, price), low: Math.min(last.low, price), close: price };\n    return [...candles.slice(0, -1), next];\n  }\n  const open = last?.close ?? price;\n  const out = [...candles, { t: bucket, open, high: Math.max(open, price), low: Math.min(open, price), close: price }];\n  return out.length > max ? out.slice(out.length - max) : out;\n}\n\n/** A clean price domain and gridlines for candles (chart-core `niceScale`, zero dropped). */\nexport function candleScale(candles: readonly Candle[], { ticks = 4 }: { ticks?: number } = {}) {\n  return niceScale(\n    candles.flatMap((c) => [c.low, c.high]),\n    { zero: false, ticks },\n  );\n}\n\nexport type CandleShape = {\n  candle: Candle;\n  /** Up (close ≥ open) or down. */\n  up: boolean;\n  /** Centre x of the candle. */\n  x: number;\n  /** Body: top y and height (at least 1px, so a doji still shows). */\n  bodyY: number;\n  bodyHeight: number;\n  /** The open's y: grow a body from here for the \"candles grow from their open\" entrance. */\n  openY: number;\n  /** Wick top (high) and bottom (low). */\n  wickTop: number;\n  wickBottom: number;\n};\n\n/**\n * Pixel geometry for candles in a `width × height` plot, on chart-core's `bands` (bodies capped\n * at `maxBar` px with air between) and `linear` y. Pass the `domain` you are springing to so\n * the candles and gridlines move together.\n */\nexport function candleLayout(\n  candles: readonly Candle[],\n  { width, height, domain = candleScale(candles).domain, maxBar = 12 }: { width: number; height: number; domain?: Domain; maxBar?: number },\n) {\n  const b = bands(candles.length, width, { maxBar, gap: 0.36 });\n  const y = linear(domain, [height, 0]);\n  const shapes: CandleShape[] = candles.map((c, i) => {\n    const up = c.close >= c.open;\n    const top = y(Math.max(c.open, c.close));\n    const bottom = y(Math.min(c.open, c.close));\n    return {\n      candle: c,\n      up,\n      x: b.center(i),\n      bodyY: top,\n      bodyHeight: Math.max(1, bottom - top),\n      openY: y(c.open),\n      wickTop: y(c.high),\n      wickBottom: y(c.low),\n    };\n  });\n  return { shapes, bar: b.bar, band: b.band, y, domain };\n}\n",
      "type": "registry:lib",
      "target": "@components/eased/_primitives/realtime-core/candles.ts"
    },
    {
      "path": "src/registry/eased/_primitives/realtime-core/clock.ts",
      "content": "\"use client\";\n\nimport { useInView } from \"motion/react\";\nimport { useCallback, useEffect, useRef, useState, useSyncExternalStore, type RefObject } from \"react\";\nimport { useSafeReducedMotion } from \"../../lib/motion\";\n\nexport type ClockOptions = {\n  /** Ms between ticks. Default 1000. */\n  intervalMs?: number;\n  /** Controlled pause (a pause button's state). Leave it out to use the returned `pause`/`resume`. */\n  paused?: boolean;\n  /** Uncontrolled initial pause. Default false. */\n  defaultPaused?: boolean;\n  /**\n   * What `prefers-reduced-motion` does: `slow` ticks 4× less often (values still update, so the\n   * data stays live, but far less flashes), `pause` waits for an explicit `resume`, `none`\n   * ignores it. Default `slow`.\n   */\n  reducedMotion?: \"slow\" | \"pause\" | \"none\";\n  /** Pause while this element is off screen (rule 85). */\n  target?: RefObject<Element | null>;\n  /** Stop for good after this many ticks: a scripted replay that ends. */\n  maxTicks?: number;\n};\n\nexport type Clock = {\n  /** Ticks elapsed since mount: 0 on the server and the first client render. */\n  tick: number;\n  /** True while ticks are being emitted. */\n  running: boolean;\n  /** Why it isn't running, if it isn't. */\n  reason: \"paused\" | \"hidden\" | \"offscreen\" | \"reduced\" | \"ended\" | null;\n  /** The user paused it (controlled or not). */\n  paused: boolean;\n  pause(): void;\n  resume(): void;\n  toggle(): void;\n  /** Back to tick 0. */\n  reset(): void;\n  /** `prefers-reduced-motion`, read after hydration: skip flashes and rolls when true. */\n  reduced: boolean;\n  /** The interval actually in use (slowed under reduced motion). */\n  intervalMs: number;\n};\n\nconst subscribeVisibility = (onChange: () => void) => {\n  document.addEventListener(\"visibilitychange\", onChange);\n  return () => document.removeEventListener(\"visibilitychange\", onChange);\n};\nconst useDocumentVisible = () =>\n  useSyncExternalStore(subscribeVisibility, () => document.visibilityState !== \"hidden\", () => true);\n\n/**\n * A pausable tick counter for live data. It stops in a hidden tab and, given a `target`, while\n * that element is off screen; under reduced motion it slows (or pauses, if asked). It is a\n * counter, not a timestamp, so a seeded stream at `tick` renders the same on every machine.\n *\n *   const clock = useClock({ intervalMs: 1200, target: ref });\n *   <button onClick={clock.toggle}>{clock.paused ? \"Resume\" : \"Pause\"}</button>\n */\nexport function useClock({\n  intervalMs = 1000,\n  paused: pausedProp,\n  defaultPaused = false,\n  reducedMotion = \"slow\",\n  target,\n  maxTicks,\n}: ClockOptions = {}): Clock {\n  const reduced = useSafeReducedMotion();\n  const visible = useDocumentVisible();\n  const none = useRef<Element | null>(null);\n  const inView = useInView(target ?? none, { initial: !target });\n  const [pausedState, setPaused] = useState(defaultPaused);\n  // Reduced-motion \"pause\" holds until the user resumes once.\n  const [reducedResumed, setReducedResumed] = useState(false);\n  const [tick, setTick] = useState(0);\n\n  const paused = pausedProp ?? pausedState;\n  const ended = maxTicks !== undefined && tick >= maxTicks;\n  const reducedHold = reduced && reducedMotion === \"pause\" && !reducedResumed;\n  const reason: Clock[\"reason\"] = paused\n    ? \"paused\"\n    : ended\n      ? \"ended\"\n      : !visible\n        ? \"hidden\"\n        : target && !inView\n          ? \"offscreen\"\n          : reducedHold\n            ? \"reduced\"\n            : null;\n  const running = reason === null;\n  const interval = reduced && reducedMotion === \"slow\" ? intervalMs * 4 : intervalMs;\n\n  useEffect(() => {\n    if (!running) return;\n    const id = window.setInterval(() => {\n      setTick((t) => (maxTicks !== undefined && t >= maxTicks ? t : t + 1));\n    }, interval);\n    return () => clearInterval(id);\n  }, [running, interval, maxTicks]);\n\n  const pause = useCallback(() => setPaused(true), []);\n  const resume = useCallback(() => {\n    setPaused(false);\n    setReducedResumed(true);\n  }, []);\n  const toggle = useCallback(() => {\n    setPaused((p) => !p);\n    setReducedResumed(true);\n  }, []);\n  const reset = useCallback(() => setTick(0), []);\n\n  return { tick, running, reason, paused, pause, resume, toggle, reset, reduced, intervalMs: interval };\n}\n",
      "type": "registry:lib",
      "target": "@components/eased/_primitives/realtime-core/clock.ts"
    },
    {
      "path": "src/registry/eased/_primitives/realtime-core/flash.ts",
      "content": "\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\n\nexport type FlashDirection = \"up\" | \"down\";\n\n/** How long a changed cell stays washed: 0.9s, long enough to catch, short enough not to pile up. */\nexport const FLASH_MS = 900;\n\n/** `up`, `down`, or null when the change is within `epsilon` (or there is no previous value). */\nexport function flashDirection(prev: number | undefined, next: number, epsilon = 0): FlashDirection | null {\n  if (prev === undefined || !Number.isFinite(prev) || !Number.isFinite(next)) return null;\n  if (next - prev > epsilon) return \"up\";\n  if (prev - next > epsilon) return \"down\";\n  return null;\n}\n\nexport type Flash = {\n  /** The direction of the latest change while it is washing, else null. */\n  direction: FlashDirection | null;\n  /** Increments on every change: key a one-shot wash element by it to restart the wash. */\n  key: number;\n};\n\ntype FlashState = { values: Record<string, number>; flashes: Record<string, Flash> };\n\n/**\n * Flash-on-change for many cells at once (an FX board, an order book): each id reports `up` or\n * `down` for `duration` ms after its value changes, then null. The first render reports\n * nothing, so a board doesn't flash on mount. Colour only: pair it with `bg-success-subtle` /\n * `bg-danger-subtle` on a background-colour transition, which reduced motion may keep.\n *\n *   const flashes = useFlashMap(Object.fromEntries(rows.map((r) => [r.id, r.price])));\n *   <td data-flash={flashes[row.id]?.direction ?? undefined}>\n */\nexport function useFlashMap(values: Record<string, number>, { duration = FLASH_MS, epsilon = 0 } = {}): Record<string, Flash> {\n  const [state, setState] = useState<FlashState>(() => ({ values, flashes: {} }));\n\n  // Adjust state while rendering when the inputs change (React's documented pattern), so the\n  // wash starts on the same frame as the new value, not one effect later.\n  let changed: Record<string, Flash> | null = null;\n  for (const id in values) {\n    const dir = flashDirection(state.values[id], values[id], epsilon);\n    if (dir || state.values[id] !== values[id]) {\n      changed ??= { ...state.flashes };\n      if (dir) changed[id] = { direction: dir, key: (state.flashes[id]?.key ?? 0) + 1 };\n    }\n  }\n  if (changed) setState({ values, flashes: changed });\n\n  const flashes = changed ?? state.flashes;\n  const signature = Object.entries(flashes)\n    .filter(([, f]) => f.direction)\n    .map(([id, f]) => `${id}:${f.key}`)\n    .join(\"|\");\n\n  // One timer per wash, so each cell clears `duration` after its own change.\n  const timers = useRef(new Map<string, number>());\n  useEffect(() => {\n    if (!signature) return;\n    for (const part of signature.split(\"|\")) {\n      const cut = part.lastIndexOf(\":\");\n      const id = part.slice(0, cut);\n      const key = Number(part.slice(cut + 1));\n      const scheduled = `${id}:${key}`;\n      if (timers.current.has(scheduled)) continue;\n      const timer = window.setTimeout(() => {\n        timers.current.delete(scheduled);\n        setState((s) =>\n          s.flashes[id]?.key === key ? { ...s, flashes: { ...s.flashes, [id]: { direction: null, key } } } : s,\n        );\n      }, duration);\n      timers.current.set(scheduled, timer);\n    }\n  }, [signature, duration]);\n  useEffect(() => {\n    const map = timers.current;\n    return () => {\n      for (const t of map.values()) clearTimeout(t);\n      map.clear();\n    };\n  }, []);\n\n  return flashes;\n}\n\n/** Flash-on-change for one value. */\nexport function useFlash(value: number, options?: { duration?: number; epsilon?: number }): Flash {\n  const flashes = useFlashMap({ v: value }, options);\n  return flashes.v ?? { direction: null, key: 0 };\n}\n",
      "type": "registry:lib",
      "target": "@components/eased/_primitives/realtime-core/flash.ts"
    },
    {
      "path": "src/registry/eased/_primitives/realtime-core/orderbook.ts",
      "content": "import { createRng, roundTo, seedFrom } from \"./rng\";\n\nexport type BookLevel = { price: number; size: number };\n/** Bids best (highest) first, asks best (lowest) first. */\nexport type OrderBook = { bids: BookLevel[]; asks: BookLevel[] };\n\nexport type DepthLevel = BookLevel & {\n  /** Cumulative size from the best price out to this level. */\n  total: number;\n  /** `total` over the larger side's deepest total, 0–1: the depth bar's width. */\n  share: number;\n  /** The level's size changed since the previous book (see `depth(book, { previous })`). */\n  changed: boolean;\n};\n\nexport type Depth = {\n  bids: DepthLevel[];\n  asks: DepthLevel[];\n  /** Midpoint of the best bid and ask. */\n  mid: number;\n  spread: number;\n  /** Spread in basis points of the mid. */\n  spreadBps: number;\n  maxTotal: number;\n};\n\nconst levelKey = (l: BookLevel) => l.price.toFixed(10);\n\n/**\n * The depth model for an order book: cumulative totals, bar shares on one scale for both sides\n * (so the sides compare), the spread, and which levels changed since `previous`.\n */\nexport function depth(book: OrderBook, { levels = 12, previous }: { levels?: number; previous?: OrderBook } = {}): Depth {\n  const prev = new Map<string, number>();\n  if (previous) for (const l of [...previous.bids, ...previous.asks]) prev.set(levelKey(l), l.size);\n  const side = (ls: BookLevel[]) => {\n    let total = 0;\n    return ls.slice(0, levels).map((l) => {\n      total += l.size;\n      const before = prev.get(levelKey(l));\n      return { ...l, total, share: 0, changed: previous !== undefined && before !== l.size };\n    });\n  };\n  const bids = side(book.bids);\n  const asks = side(book.asks);\n  const maxTotal = Math.max(bids[bids.length - 1]?.total ?? 0, asks[asks.length - 1]?.total ?? 0) || 1;\n  for (const l of [...bids, ...asks]) l.share = l.total / maxTotal;\n  const bestBid = bids[0]?.price ?? 0;\n  const bestAsk = asks[0]?.price ?? bestBid;\n  const mid = (bestBid + bestAsk) / 2;\n  const spread = bestAsk - bestBid;\n  return { bids, asks, mid, spread, spreadBps: mid ? (spread / mid) * 10_000 : 0, maxTotal };\n}\n\n/** Cumulative depth curves (price, total) for a depth chart, each side from the spread outwards. */\nexport function depthCurve(d: Depth) {\n  return {\n    bids: d.bids.map((l) => [l.price, l.total] as const),\n    asks: d.asks.map((l) => [l.price, l.total] as const),\n  };\n}\n\nexport type SyntheticBookOptions = {\n  seed: number | string;\n  /** The mid price, e.g. from a `TickStream` tick. */\n  mid: number;\n  /** The book's tick index: consecutive indexes change only some levels. */\n  index: number;\n  /** Price increment. Default 0.01. */\n  tickSize?: number;\n  /** Levels per side. Default 14. */\n  levels?: number;\n  /** Typical size of a level near the spread. Default 1.5. */\n  baseSize?: number;\n  /** Size decimals. Default 4. */\n  sizeDecimals?: number;\n  /** Share of levels that change per tick. Default 0.3. */\n  churn?: number;\n};\n\n/**\n * A believable, deterministic order book around `mid`: sizes grow away from the spread with a\n * few walls, the spread breathes between one and three ticks, and from one `index` to the next\n * only about `churn` of the levels change, so flash-on-change reads like a real feed.\n */\nexport function syntheticBook({ seed, mid, index, tickSize = 0.01, levels = 14, baseSize = 1.5, sizeDecimals = 4, churn = 0.3 }: SyntheticBookOptions): OrderBook {\n  const base = seedFrom(String(seed));\n  const decimals = (String(tickSize).split(\".\")[1] ?? \"\").length;\n  const changedAt = (side: number, level: number, i: number) => createRng(base ^ seedFrom(`${side}/${level}/${i}`)).next() < churn;\n  // The last index ≤ `index` at which this level changed (geometric, so a short walk back).\n  const lastChange = (side: number, level: number) => {\n    for (let i = index, n = 0; i > 0 && n < 48; i--, n++) if (changedAt(side, level, i)) return i;\n    return 0;\n  };\n  const sizeOf = (side: number, level: number) => {\n    const r = createRng(base ^ seedFrom(`size/${side}/${level}/${lastChange(side, level)}`));\n    const shape = 1 + level * 0.22;\n    const wall = r.chance(0.08) ? r.range(2.5, 4) : 1;\n    return roundTo(baseSize * shape * wall * r.range(0.35, 1.65), sizeDecimals);\n  };\n  // The spread re-rolls every few ticks, not every tick, so the price levels hold still between.\n  const halfSpread = ((1 + createRng(base ^ seedFrom(`spread/${Math.floor(index / 6)}`)).int(0, 2)) * tickSize) / 2;\n  const bestBid = Math.floor((mid - halfSpread) / tickSize) * tickSize;\n  const bestAsk = Math.max(bestBid + tickSize, Math.ceil((mid + halfSpread) / tickSize) * tickSize);\n  const bids: BookLevel[] = [];\n  const asks: BookLevel[] = [];\n  for (let l = 0; l < levels; l++) {\n    bids.push({ price: roundTo(bestBid - l * tickSize, decimals), size: sizeOf(0, l) });\n    asks.push({ price: roundTo(bestAsk + l * tickSize, decimals), size: sizeOf(1, l) });\n  }\n  return { bids, asks };\n}\n",
      "type": "registry:lib",
      "target": "@components/eased/_primitives/realtime-core/orderbook.ts"
    },
    {
      "path": "src/registry/eased/_primitives/realtime-core/realtime-core.ts",
      "content": "/**\n * realtime-core: deterministic live data for tickers, order books, candles and vitals.\n * A seeded stream, a pausable clock, flash-on-change state, and OHLC and depth models that sit\n * on chart-core's scales. Import from here; the files behind it may move.\n */\nexport { createRng, roundTo, seedFrom, type Rng } from \"./rng\";\nexport { change, createTickStream, STREAM_EPOCH, type SeriesSpec, type Tick, type TickStream, type TickStreamOptions } from \"./stream\";\nexport { useClock, type Clock, type ClockOptions } from \"./clock\";\nexport { useTicker, type Ticker, type UseTickerOptions } from \"./use-ticker\";\nexport { FLASH_MS, flashDirection, useFlash, useFlashMap, type Flash, type FlashDirection } from \"./flash\";\nexport { candleLayout, candleScale, candlesFromStream, pushPrice, toCandles, type Candle, type CandleShape } from \"./candles\";\nexport { depth, depthCurve, syntheticBook, type BookLevel, type Depth, type DepthLevel, type OrderBook, type SyntheticBookOptions } from \"./orderbook\";\n",
      "type": "registry:lib",
      "target": "@components/eased/_primitives/realtime-core/realtime-core.ts"
    },
    {
      "path": "src/registry/eased/_primitives/realtime-core/rng.ts",
      "content": "/**\n * A small seeded RNG (mulberry32). Same seed, same sequence, on the server and in every\n * browser, so seeded demos hydrate without a mismatch and look the same in every screenshot.\n */\n\nexport type Rng = {\n  /** Uniform in [0, 1). */\n  next(): number;\n  /** Uniform in [min, max). */\n  range(min: number, max: number): number;\n  /** Integer in [min, max], both inclusive. */\n  int(min: number, max: number): number;\n  /** Standard normal (mean 0, sd 1), Box–Muller. */\n  normal(): number;\n  /** True with probability `p`. */\n  chance(p: number): boolean;\n  /** One item of a non-empty list. */\n  pick<T>(items: readonly T[]): T;\n  /** A shuffled copy (Fisher–Yates). */\n  shuffle<T>(items: readonly T[]): T[];\n  /** The internal state, to resume a stream later with `createRng(state)`. */\n  state(): number;\n};\n\n/** A 32-bit seed from any string (FNV-1a), so seeds can be readable: `seedFrom(\"crypto/BTC\")`. */\nexport function seedFrom(text: string): number {\n  let h = 0x811c9dc5;\n  for (let i = 0; i < text.length; i++) {\n    h ^= text.charCodeAt(i);\n    h = Math.imul(h, 0x01000193);\n  }\n  return h >>> 0;\n}\n\n/** A seeded generator. `seed` is a number or a string (hashed with `seedFrom`). */\nexport function createRng(seed: number | string): Rng {\n  let s = (typeof seed === \"string\" ? seedFrom(seed) : seed) >>> 0;\n  const next = () => {\n    s = (s + 0x6d2b79f5) >>> 0;\n    let t = s;\n    t = Math.imul(t ^ (t >>> 15), t | 1);\n    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);\n    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n  };\n  const rng: Rng = {\n    next,\n    range: (min, max) => min + (max - min) * next(),\n    int: (min, max) => Math.floor(min + (max - min + 1) * next()),\n    normal: () => {\n      const u = 1 - next();\n      const v = next();\n      return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);\n    },\n    chance: (p) => next() < p,\n    pick: (items) => items[Math.floor(next() * items.length)],\n    shuffle: (items) => {\n      const out = [...items];\n      for (let i = out.length - 1; i > 0; i--) {\n        const j = Math.floor(next() * (i + 1));\n        [out[i], out[j]] = [out[j], out[i]];\n      }\n      return out;\n    },\n    state: () => s,\n  };\n  return rng;\n}\n\n/** Rounds to `decimals` places without float noise (0.1 + 0.2 → 0.3). */\nexport function roundTo(value: number, decimals: number): number {\n  const k = 10 ** decimals;\n  return Math.round(value * k) / k;\n}\n",
      "type": "registry:lib",
      "target": "@components/eased/_primitives/realtime-core/rng.ts"
    },
    {
      "path": "src/registry/eased/_primitives/realtime-core/stream.ts",
      "content": "import { createRng, roundTo, type Rng } from \"./rng\";\n\n/** One value the stream moves: a price, a rate, a level such as heart rate or requests/s. */\nexport type SeriesSpec = {\n  id: string;\n  /** Value at tick 0, and what `meanRevert` pulls back towards. */\n  initial: number;\n  /**\n   * `price` walks multiplicatively (log-normal, never below 0): FX, stocks, tokens.\n   * `level` walks additively around `initial`: heart rate, latency, queue depth. Default `price`.\n   */\n  kind?: \"price\" | \"level\";\n  /** Per-tick standard deviation as a share of the value (price) or of `initial` (level). Default 0.001 price (≈ 0.1% a tick), 0.02 level. */\n  volatility?: number;\n  /** Per-tick drift as a share (price) or in units (level). Default 0. */\n  drift?: number;\n  /** 0–1, how hard each tick pulls back to `initial`. Default 0.004 price, 0.08 level. */\n  meanRevert?: number;\n  /** Places the emitted values are rounded to. Default 2. */\n  decimals?: number;\n  /** Clamp. */\n  min?: number;\n  max?: number;\n};\n\nexport type Tick = {\n  /** 0, 1, 2 …: the tick's position in the stream. */\n  index: number;\n  /** Epoch ms of the tick: `start + index × intervalMs`. */\n  t: number;\n  /** Each series' value at this tick, rounded to its `decimals`. */\n  values: Record<string, number>;\n};\n\nexport type TickStreamOptions = {\n  /** Same seed, same stream, everywhere. */\n  seed: number | string;\n  series: readonly SeriesSpec[];\n  /** Spacing of tick timestamps. Default 1000. */\n  intervalMs?: number;\n  /** Epoch ms of tick 0. Default 18 Oct 2026 14:30 UTC, so output never depends on the clock. */\n  start?: number;\n};\n\nexport type TickStream = {\n  readonly series: readonly SeriesSpec[];\n  readonly intervalMs: number;\n  readonly start: number;\n  /** The tick at `index` (random access; walked once, then cached). */\n  at(index: number): Tick;\n  /** `count` ticks ending at `end` (inclusive), oldest first; clipped at tick 0. */\n  window(end: number, count: number): Tick[];\n  /** One series' values over a window, oldest first: feed it to a Sparkline or Line Chart. */\n  values(id: string, end: number, count: number): number[];\n};\n\n/** 18 Oct 2026 14:30 UTC: the fixed \"now\" every Eased fixture and stream is anchored to. */\nexport const STREAM_EPOCH = Date.UTC(2026, 9, 18, 14, 30);\n\ntype Walker = { spec: Required<Omit<SeriesSpec, \"min\" | \"max\">> & Pick<SeriesSpec, \"min\" | \"max\">; rng: Rng; raw: number[] };\n\n/**\n * A deterministic, seeded tick stream: prices, rates or metrics as random walks that hydrate\n * identically and replay the same every time. Each series has its own generator\n * (`seed/id`), so adding a series never changes the others.\n *\n *   const fx = createTickStream({ seed: \"fx\", series: [{ id: \"EURUSD\", initial: 1.0842, decimals: 4 }] });\n *   fx.at(120).values.EURUSD; // 1.0857, always\n */\nexport function createTickStream({ seed, series, intervalMs = 1000, start = STREAM_EPOCH }: TickStreamOptions): TickStream {\n  const walkers: Walker[] = series.map((s) => {\n    const kind = s.kind ?? \"price\";\n    return {\n      spec: {\n        id: s.id,\n        initial: s.initial,\n        kind,\n        volatility: s.volatility ?? (kind === \"price\" ? 0.001 : 0.02),\n        drift: s.drift ?? 0,\n        meanRevert: s.meanRevert ?? (kind === \"price\" ? 0.004 : 0.08),\n        decimals: s.decimals ?? 2,\n        min: s.min,\n        max: s.max,\n      },\n      rng: createRng(`${seed}/${s.id}`),\n      raw: [s.initial],\n    };\n  });\n  const cache = new Map<number, Tick>();\n\n  const rawAt = (w: Walker, index: number) => {\n    const { spec, rng, raw } = w;\n    while (raw.length <= index) {\n      const v = raw[raw.length - 1];\n      const z = rng.normal();\n      let next =\n        spec.kind === \"price\"\n          ? v * Math.exp(spec.drift + spec.volatility * z) + spec.meanRevert * (spec.initial - v)\n          : v + spec.drift + spec.volatility * spec.initial * z + spec.meanRevert * (spec.initial - v);\n      if (spec.kind === \"price\") next = Math.max(next, spec.initial * 1e-3);\n      if (spec.min !== undefined) next = Math.max(spec.min, next);\n      if (spec.max !== undefined) next = Math.min(spec.max, next);\n      raw.push(next);\n    }\n    return raw[index];\n  };\n\n  const at = (index: number): Tick => {\n    const i = Math.max(0, Math.floor(index));\n    const hit = cache.get(i);\n    if (hit) return hit;\n    const values: Record<string, number> = {};\n    for (const w of walkers) values[w.spec.id] = roundTo(rawAt(w, i), w.spec.decimals);\n    const tick = { index: i, t: start + i * intervalMs, values };\n    if (cache.size > 4096) cache.clear();\n    cache.set(i, tick);\n    return tick;\n  };\n\n  const slice = (end: number, count: number) => {\n    const out: Tick[] = [];\n    for (let i = Math.max(0, end - count + 1); i <= end; i++) out.push(at(i));\n    return out;\n  };\n\n  return {\n    series,\n    intervalMs,\n    start,\n    at,\n    window: slice,\n    values: (id, end, count) => slice(end, count).map((t) => t.values[id]),\n  };\n}\n\n/** Change between two values: absolute, as a share of `from`, and the direction. */\nexport function change(from: number, to: number) {\n  const delta = to - from;\n  return { delta, pct: from === 0 ? 0 : delta / Math.abs(from), direction: delta > 0 ? \"up\" : delta < 0 ? \"down\" : \"flat\" } as const;\n}\n",
      "type": "registry:lib",
      "target": "@components/eased/_primitives/realtime-core/stream.ts"
    },
    {
      "path": "src/registry/eased/_primitives/realtime-core/use-ticker.ts",
      "content": "\"use client\";\n\nimport { useMemo } from \"react\";\nimport { useClock, type Clock, type ClockOptions } from \"./clock\";\nimport type { Tick, TickStream } from \"./stream\";\n\nexport type UseTickerOptions = ClockOptions & {\n  /** The stream index shown at mount, so a chart opens with history. Default `history - 1`. */\n  startIndex?: number;\n  /** How many ticks `history` keeps, oldest first. Default 60. */\n  history?: number;\n};\n\nexport type Ticker = Clock & {\n  /** The stream index on screen now: `startIndex + clock.tick`. */\n  index: number;\n  /** The current tick. */\n  current: Tick;\n  /** The tick before it (same as `current` at index 0). */\n  previous: Tick;\n  /** The last `history` ticks, oldest first. */\n  history: Tick[];\n  /** One series over `history`, oldest first: hand it to a Sparkline or Line Chart as `values`. */\n  series(id: string): number[];\n};\n\n/**\n * Drives a seeded `TickStream` from a `useClock`: the live board's one hook. Hydration-safe,\n * because the first render on both sides is `startIndex`, and deterministic, because every\n * value comes from the seed, never from `Math.random` or the wall clock.\n *\n *   const stream = useMemo(() => createTickStream({ seed: \"fx\", series }), []);\n *   const fx = useTicker(stream, { intervalMs: 1500, target: ref });\n *   fx.current.values.EURUSD; fx.series(\"EURUSD\"); fx.toggle();\n */\nexport function useTicker(stream: TickStream, { startIndex, history = 60, ...clockOptions }: UseTickerOptions = {}): Ticker {\n  const clock = useClock({ intervalMs: stream.intervalMs, ...clockOptions });\n  const index = (startIndex ?? history - 1) + clock.tick;\n  const slice = useMemo(() => stream.window(index, history), [stream, index, history]);\n  const current = slice[slice.length - 1];\n  const previous = slice[slice.length - 2] ?? current;\n  return {\n    ...clock,\n    index,\n    current,\n    previous,\n    history: slice,\n    series: (id) => slice.map((t) => t.values[id]),\n  };\n}\n",
      "type": "registry:lib",
      "target": "@components/eased/_primitives/realtime-core/use-ticker.ts"
    }
  ],
  "type": "registry:lib"
}