"use client"; import { motion, useReducedMotion } from "motion/react"; import { createContext, type ReactNode, useCallback, useContext, useLayoutEffect, useRef, useState, } from "react"; import { SPRING_PANEL } from "@/lib/ease"; import { cn } from "@/lib/utils"; import { OrderBookDepthRow } from "./order-book/depth-row"; import { buildOrderBook, type DepthLevel, summarizeDepth } from "./order-book/model"; export type OrderBookLevel = { price: number; size: number }; export type { DepthLevel as OrderBookDepthLevel, DepthSummary as OrderBookDepthSummaryData } from "./order-book/model"; /** A level identified by its side and price, so it survives snapshot updates. */ export type OrderBookActiveLevel = { side: "bid" | "ask"; price: number } | null; export interface OrderBookProps { /** Snapshot of buy levels. Equal prices are combined; invalid and zero sizes are omitted. */ bids: readonly OrderBookLevel[]; /** Snapshot of sell levels. Supply new arrays when the book changes. */ asks: readonly OrderBookLevel[]; /** Number of nearest levels to show on each side. */ levels?: number; /** Last traded price. Omit to show the midpoint of the best bid and ask. */ lastPrice?: number; label?: string; baseSymbol?: string; quoteSymbol?: string; formatPrice?: (value: number) => string; formatSize?: (value: number) => string; /** * The level whose depth is inspected: it and every better level on its side * are highlighted, and `OrderBookDepthSummary` totals them. Controlled. */ activeLevel?: OrderBookActiveLevel; defaultActiveLevel?: OrderBookActiveLevel; onActiveLevelChange?: (level: OrderBookActiveLevel) => void; /** Makes each price a button, for example to fill an order ticket. */ onLevelSelect?: (level: DepthLevel, side: "bid" | "ask") => void; children?: ReactNode; className?: string; } const priceFormatter = new Intl.NumberFormat("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2, }); const sizeFormatter = new Intl.NumberFormat("en-US", { notation: "compact", maximumFractionDigits: 1, }); const defaultPrice = (value: number) => priceFormatter.format(value); const defaultSize = (value: number) => sizeFormatter.format(value); function useOrderBookModel({ bids, asks, levels = 9, lastPrice, baseSymbol = "", quoteSymbol = "USD", formatPrice = defaultPrice, formatSize = defaultSize, activeLevel, defaultActiveLevel = null, onActiveLevelChange, onLevelSelect, }: OrderBookProps) { const book = buildOrderBook(bids, asks, levels); const [internalActive, setInternalActive] = useState(defaultActiveLevel); const rawActive = activeLevel === undefined ? internalActive : activeLevel; // Resolve the inspected level during render by price; a level that left the // book clears the inspection instead of pointing at a different row. const activeIndex = rawActive ? (rawActive.side === "bid" ? book.bids : book.asks).findIndex( (level) => level.price === rawActive.price, ) : -1; const active = rawActive && activeIndex >= 0 ? { side: rawActive.side, index: activeIndex } : null; // Stable setter: it reads the latest controlled flag and callback from refs. const latest = useRef({ controlled: activeLevel !== undefined, onActiveLevelChange }); useLayoutEffect(() => { latest.current = { controlled: activeLevel !== undefined, onActiveLevelChange }; }); const setActiveLevel = useCallback((level: OrderBookActiveLevel) => { if (!latest.current.controlled) setInternalActive(level); latest.current.onActiveLevelChange?.(level); }, []); return { ...book, active, summary: active ? summarizeDepth((active.side === "bid" ? book.bids : book.asks).slice(0, active.index + 1), active.side) : null, setActiveLevel, onLevelSelect, price: lastPrice != null && Number.isFinite(lastPrice) && lastPrice > 0 ? lastPrice : book.midpoint, baseSymbol, quoteSymbol, formatPrice, formatSize, reduce: useReducedMotion(), }; } const OrderBookContext = createContext | null>(null); export function useOrderBook() { const context = useContext(OrderBookContext); if (!context) throw new Error("Order book parts must be rendered inside OrderBook."); return context; } /** A snapshot-driven depth ladder. No timers or invented market data live in the chart. */ export function OrderBook({ children, className, label = "Order book", ...props }: OrderBookProps) { const model = useOrderBookModel(props); return (
{children === undefined ? ( <> ) : ( children )}
); } export function OrderBookHeader({ className }: { className?: string }) { const { baseSymbol, quoteSymbol } = useOrderBook(); return ( ); } export function OrderBookSide({ side, className }: { side: "bid" | "ask"; className?: string }) { const { bids, asks, maxTotal, formatPrice, formatSize, reduce, baseSymbol, quoteSymbol, active, setActiveLevel, onLevelSelect, } = useOrderBook(); const rows = side === "ask" ? [...asks].reverse() : bids; const color = side === "ask" ? "text-rose-600 dark:text-rose-400" : "text-emerald-700 dark:text-emerald-400"; return ( setActiveLevel(null)}> {rows.length === 0 ? ( ) : ( rows.map((row, index) => { const depthIndex = side === "ask" ? rows.length - 1 - index : index; return ( setActiveLevel(inspect ? { side, price: row.price } : null) } onSelect={onLevelSelect ? () => onLevelSelect(row, side) : undefined} fraction={maxTotal ? row.total / maxTotal : 0} entranceIndex={side === "ask" ? rows.length - 1 - index : index} color={color} formatPrice={formatPrice} formatSize={formatSize} reduce={!!reduce} /> ); }) )}
Price {quoteSymbol} Size {baseSymbol} Cumulative total {baseSymbol}
No {side === "ask" ? "asks" : "bids"}
); } export function OrderBookSpread({ className }: { className?: string }) { const { price, spread, midpoint, formatPrice } = useOrderBook(); return (
Reference price: {price == null ? "—" : formatPrice(price)} {spread == null || midpoint == null ? "Spread unavailable" : spread < 0 ? "Crossed book" : `Spread ${formatPrice(spread)} · ${((spread / midpoint) * 100).toFixed(3)}%`}
); } /** Balance of the displayed depth, not the entire exchange order book. */ export function OrderBookBalance({ className }: { className?: string }) { const { bidTotal, askTotal, reduce } = useOrderBook(); const total = bidTotal + askTotal; const ratio = total ? bidTotal / total : 0.5; return (
Bids {total ? `${(ratio * 100).toFixed(1)}%` : "—"} Visible depth Asks {total ? `${((1 - ratio) * 100).toFixed(1)}%` : "—"}
); } /** * Totals the inspected levels: the hovered or focused level and every better * level on its side, as the cost of filling an order up to that price. */ export function OrderBookDepthSummary({ placeholder = "Hover a level to see the cost of filling up to it", children, className, }: { placeholder?: ReactNode; /** Custom readout for the inspected levels. */ children?: (summary: NonNullable["summary"]>) => ReactNode; className?: string; }) { const { summary, formatPrice, formatSize, baseSymbol, quoteSymbol } = useOrderBook(); return (

{!summary ? ( {placeholder} ) : children ? ( children(summary) ) : ( <> Avg {formatPrice(summary.averagePrice)} {formatSize(summary.size)} {baseSymbol && ` ${baseSymbol}`} {" · "} {formatPrice(summary.quote)} {quoteSymbol && ` ${quoteSymbol}`} )}

); }