Bracket Replay
A four-state playable knockout bracket — a 16-entrant draw revealed round by round with play/pause, a round scrubber, seed-aware upset chips, structural feed repair and a champion cell that fills at the end.
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/chart-bracket-replay.jsonPrompt
Build a React + TypeScript + Tailwind "ChartBracketReplay" card — a knockout
draw (racket-sport bracket) replayed round by round, drawn as absolutely
positioned match cards with hand-rolled SVG elbow connectors (no chart
library) and zod for the contract.
Contract
- A zod schema is the single source of truth:
{ status: "loading" | "empty" | "error" | "ready";
title?: string;
rounds: { label ("R16" | "QF" | "SF" | "F" …),
matches: { a: { name, seed? }, b: { name, seed? },
winner: "a" | "b", score? }[] }[] }
with structural refines: the first round holds at most 8 matches
(16 entrants — readability cap), every later round holds exactly half the
previous round's matches, and the last round is a single final so "the
champion" is well defined. Component props = z.infer of that schema plus
onRetry?, emptyState? and className; no parallel hand-written interface.
- Name continuity between rounds is deliberately NOT a refine: an
inconsistent feed must still be drawable so the inconsistency can be shown.
Behavior
- Four first-class branches inside one bg-card panel: loading (a skeleton of
shrinking card columns keeping the bracket silhouette, aria-hidden, plus an
sr-only role=status), empty ("draw not released" over a bare-bracket
glyph), error (destructive copy + "Try again" only when onRetry exists),
and ready.
- Draw maths live in one exported pure function. It walks the rounds
BACKWARD validating that every participant equals the recorded winner of
the feeder match below; on mismatch it trusts the LATER round, relabels the
earlier winner (name and seed, cascading toward round one) and returns one
repair sentence per fix — the card prints them under the bracket, never a
contradictory line silently. It also flags upsets: winner seed number
strictly greater than loser seed, both seeded.
- One timeline position = revealed round count (a float, so matches inside
the round being revealed stagger ~90 ms apart as a sub-position). Rounds at
or below the position show winners; rounds above it show pairings only.
- NO autoplay: ready parks fully revealed with the champion visible — a
deterministic first paint with no effects. Play from the end rewinds to
round 0 (bare first-round pairings, no winners) and replays forward; Play
mid-timeline resumes, Pause holds.
- Playback is requestAnimationFrame with timestamp deltas — never
setInterval — cancelled on pause, speed change, data swap and unmount.
Speed toggle 0.5× / 1× / 2× scales the deltas.
- prefers-reduced-motion (matchMedia, listener cleaned up): whole rounds
reveal at once on a fixed cadence, no stagger and no tweens
(motion-reduce:transition-none on every transition).
- A polite live region says one sentence per round boundary ("QF decided."),
and at the end names the champion — throttled by construction.
- Controls: Play/Pause button with aria-pressed and a state-aware aria-label,
a labelled native range scrubber over whole rounds (seek anytime, seeking
pauses; aria-valuetext names the round) with an aria-hidden label rail
underneath, and a role=radiogroup speed toggle.
- Hover or focus on a match card shows a tooltip (full names, seeds, score,
upset flag, or "not yet revealed"); the cards form one roving-tabindex list
— a single tab stop, ArrowUp/Down walking column by column, ArrowRight
jumping to the match this one feeds, ArrowLeft to the feeder below,
Home/End to the ends. Pointer-leave hands the tooltip back to the focused
card. An sr-only table repeats every result in full, independent of how far
the reveal has been scrubbed.
Rendering & styling
- Fixed pixel geometry: one column per round plus a champion cell, match
cards 168×72 on a pitch that doubles per round so every match sits centred
between the two that feed it; nothing is measured from the DOM, so SSR
paints the finished picture. The stage pans horizontally inside the card
(min-w-0 + overflow-x-auto) and never blows out the page.
- SVG elbow connectors in border ink between rounds (M → H mid → V → H); a
connector strengthens (muted-foreground stroke, heavier width) once its
feeder match is decided, and the final's line to the champion cell does the
same.
- Decided matches: winner name font-semibold behind a 2px side bar in
var(--chart-1), loser muted, score in muted tabular text; upsets wear a
small chip tinted by color-mix of var(--chart-4) with the card. Undecided
matches print both names in plain foreground with the score line reserved
(opacity 0) so nothing jumps when it lands.
- The champion cell sits dashed and empty ("to be revealed") until the final
is decided, then fills with the champion's name and a 2px ring drawn as a
box-shadow in var(--chart-1).
- Readouts: the current round label as the big figure, "round r / R" and
"n upsets so far" beside it; column headers brighten as their round
decides.
- Semantic tokens only: bg-card, bg-muted, border, text-muted-foreground,
text-destructive, bg-popover, accent-primary on the range input,
var(--chart-1) and var(--chart-4) — never raw hex/rgb/oklch. cn() merges
className; remaining props spread on the root; forwardRef to the panel.
Customization levers
- Density: CARD_W / CARD_H / COL_GAP / ROW_GAP set the whole geometry from
four constants — shrink them for an embeddable mini-bracket, or drop the
score line and keep results in the tooltip only.
- Pace: STAGGER_MS (per match), HOLD_MS (pause on a decided round) and the
SPEEDS array tune the replay; keep the reduced-motion whole-round path.
- Palette: the winner bar and champion ring read var(--chart-1), the upset
chip var(--chart-4) — remap to any chart tokens, but keep text in text
tokens and connectors in border ink.
- Vocabulary: round labels are the feed's own ("R16" or "Round of 16"); the
live region and readouts inherit them. Works for any single-elimination
racket sport — swap set strings for badminton or squash game scores.
- Repair policy: trusting the later round is one branch of the pure layout
function; a consumer who trusts the earlier round instead flips which side
is relabelled and keeps the visible report.Concepts
- Timeline as revealed rounds — the whole replay hangs on one number: how many rounds show their results. Scrub stops are whole rounds, playback interpolates between them so matches stagger in, and every integer position renders identically whether it was scrubbed to or played through — deterministic, resumable, and honest under reduced motion.
- No autoplay, parked at the end — the ready card's first paint is the finished draw with the champion ringed, because a recap page must answer "who won" before it performs. Play is an explicit rewind-and-reveal, never something the card decides to do to a reader who just scrolled past.
- Trust the later round — when a name arrives in round r that the round below never sent up, the feed contradicts itself and one side must win. The later round is closer to the result being displayed, so the earlier winner is relabelled — cascading back toward round one so no connector ever joins two cards that disagree — and every repair is printed under the bracket instead of being swallowed.
- Seeded upset detection — an upset chip appears only when a seeded winner beats a better seed (winner's seed number strictly greater). An unseeded player beating a seed carries no seed number to invert, so it is narrated in the tooltip rather than flagged — the chip measures the seeding being wrong, not the result being surprising.
- Connectors that decide with the match — the elbow from a match strengthens the moment that match reveals its winner, so the champion's path literally darkens across the board as the replay runs; before that, border-ink elbows only promise the structure.
- Keyboard walks the draw, not the DOM — one tab stop, arrows up and down the columns, and left/right move along the connectors themselves: right jumps to the match this winner feeds, left drops to the feeder below, which is how a bracket is actually read.
Coverage Heat Replay
A four-state, scrubbable tennis coverage heatmap — presence time per half-metre cell accumulating over the match clock in hand-rolled SVG, with play/pause and speed controls, set markers on the rail, a colour scale fixed against the full match, and a busiest-zone readout recomputed at any moment.
Serve Speed Replay
A four-state tennis serve-speed replay in hand-rolled SVG — a 240° dial whose needle kicks to each serve while a fixed-band histogram builds up one tick at a time, with play/pause, an index scrubber, speed gears, a persistent fastest-so-far ring marker and a scrub-time readout row.