# Split-Flap Station Board Countdown That Clatters Down to a Launch Date

> Countdown built as a departure board: split flaps behind glass clatter through their drums to show days, hours and seconds. Free React and HTML code.

A shadcn/ui block from the [@weinc](https://we.inc/components?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/weinc/split-flap-station-board-countdown-that-clatters-down-to-a-launch-date).

- Page, with a live preview: https://shadcn.exchange/r/weinc/split-flap-station-board-countdown-that-clatters-down-to-a-launch-date
- Screenshot: https://shadcn.exchange/shot/weinc/split-flap-station-board-countdown-that-clatters-down-to-a-launch-date/light.png
- Registry item: https://we.inc/r/split-flap-station-board-countdown-that-clatters-down-to-a-launch-date.json
- Listed: 2026-10-28, source last changed 2026-10-28

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/weinc/split-flap-station-board-countdown-that-clatters-down-to-a-launch-date.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Files

### `components/we-inc/split-flap-station-board-countdown-that-clatters-down-to-a-launch-date.tsx`

```tsx
import { useEffect, useRef, useState } from "react";

// When the countdown ends, in the visitor's local time ("2027-03-01T09:00:00").
// Left empty, or once the date has passed, the board counts to DEMO_DAYS after the first view.
const TARGET = "";
const DEMO_DAYS = 12.3212;
// The top row turns through these. Twelve characters at most, capitals, digits and spaces.
const messages = ["LAUNCHING IN", "ATLAS TWO", "ALL ABOARD"];
const EVENT = "Atlas Two";

const W = 860, H = 560, OX = 50; // drawing size and the left margin kept for the shadow; the canvas scales to its box
const L_DRUM = " ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789:"; // order of the flaps on a letter drum
const D_DRUM = "9876543210"; // number drums are wound backwards, so counting down is one flap forward
const SIZES = [[50, 62], [70, 104], [37, 48]]; // flap width and height for the message, number and date rows
const DAYS = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"];
const MONTHS = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"];

type Cell = { x: number; y: number; kind: number; n: number; shown: number; next: number; goal: number; left: number; p: number; dur: number; wait: number; kicked: number };
const pad2 = (v: number) => String(v).padStart(2, "0");
const mod = (a: number, n: number) => ((a % n) + n) % n;
const centre = (text: string, width: number) => { const t = text.slice(0, width); const l = Math.floor((width - t.length) / 2); return (" ".repeat(l) + t).padEnd(width, " "); };
const split = (ms: number) => { const s = Math.max(0, Math.floor(ms / 1000)); return { d: Math.min(99, Math.floor(s / 86400)), h: Math.floor(s / 3600) % 24, m: Math.floor(s / 60) % 60, s: s % 60 }; };

export default function SplitFlapStationBoardCountdown() {
  const canvas = useRef<HTMLCanvasElement>(null);
  const api = useRef<{ shuffle: (x: number, y: number) => void } | null>(null);
  const [info, setInfo] = useState<{ left: number; when: string } | null>(null);

  useEffect(() => {
    const cv = canvas.current;
    if (!cv) return;
    const ctx = cv.getContext("2d");
    if (!ctx) return;
    const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    const parsed = TARGET ? new Date(TARGET).getTime() : NaN;
    const end = Number.isFinite(parsed) && parsed > Date.now() ? parsed : Math.ceil((Date.now() + DEMO_DAYS * 86400000) / 3600000) * 3600000;
    const ed = new Date(end);
    const dateLine = `${DAYS[ed.getDay()]} ${pad2(ed.getDate())} ${MONTHS[ed.getMonth()]} ${pad2(ed.getHours())}:${pad2(ed.getMinutes())}`;
    const when = `${DAYS[ed.getDay()]} ${ed.getDate()} ${MONTHS[ed.getMonth()]}, ${pad2(ed.getHours())}:${pad2(ed.getMinutes())}`;

    // Every flap position on the board.
    const cells: Cell[] = [];
    const row = (kind: number, count: number, x0: number, y: number, gap: number, pairGap: number) => {
      const drum = kind === 1 ? D_DRUM : L_DRUM;
      let x = x0;
      for (let i = 0; i < count; i++) {
        cells.push({ x: x + OX, y, kind, n: drum.length, shown: kind === 1 ? (i * 7 + 3) % 10 : 0, next: 0, goal: 0, left: 0, p: -1, dur: 60, wait: 0, kicked: 0 });
        x += SIZES[kind][0] + (pairGap && i % 2 === 1 ? pairGap : gap);
      }
    };
    row(0, 12, 52.5, 134, 5, 0);
    row(1, 8, 51, 212, 5, 26);
    row(2, 16, 54, 352, 4, 0);
    const top = cells.slice(0, 12), nums = cells.slice(12, 20), date = cells.slice(20);

    const aim = (c: Cell, idx: number, spin: number, wait: number) => {
      if (reduce) { c.shown = idx; c.goal = idx; c.left = 0; c.p = -1; return; }
      const flying = c.p >= 0;
      const fwd = mod(idx - (flying ? c.next : c.shown), c.n);
      c.goal = idx;
      c.left = (c.kind === 1 ? (fwd === 0 && spin ? c.n : fwd) : fwd === 0 && !spin ? 0 : Math.max(1, spin)) + (flying ? 1 : 0);
      if (!flying) c.wait = wait;
    };
    const write = (set: Cell[], text: string, spin: number, stagger: number, base: number) =>
      set.forEach((c, i) => { const idx = Math.max(0, (c.kind === 1 ? D_DRUM : L_DRUM).indexOf(text[i] || " ")); if (idx !== c.goal || spin) aim(c, idx, spin ? spin + (i % 3) : 0, base + i * stagger); });

    let S = 1, bg: HTMLCanvasElement | null = null;
    let atlas: { a: HTMLCanvasElement; tw: number; th: number }[] = [];
    const rr = (g: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, r: number) => { g.beginPath(); g.moveTo(x + r, y); g.arcTo(x + w, y, x + w, y + h, r); g.arcTo(x + w, y + h, x, y + h, r); g.arcTo(x, y + h, x, y, r); g.arcTo(x, y, x + w, y, r); g.closePath(); };

    // One flap card: matt black, a hinge across the middle, the pins that carry it at either side.
    const paintFlap = (g: CanvasRenderingContext2D, w: number, h: number, ch: string) => {
      rr(g, 0, 0, w, h, Math.min(4, w * 0.08)); g.save(); g.clip();
      const a = g.createLinearGradient(0, 0, 0, h / 2); a.addColorStop(0, "#303135"); a.addColorStop(1, "#1f2023");
      g.fillStyle = a; g.fillRect(0, 0, w, h / 2);
      const b = g.createLinearGradient(0, h / 2, 0, h); b.addColorStop(0, "#111213"); b.addColorStop(0.22, "#1b1c1e"); b.addColorStop(1, "#27282b");
      g.fillStyle = b; g.fillRect(0, h / 2, w, h / 2);
      const px = h * 0.74;
      g.font = `700 ${px}px "Helvetica Neue", Helvetica, Arial, sans-serif`; g.textAlign = "center"; g.textBaseline = "alphabetic";
      const squeeze = Math.min(0.88, (w * 0.76) / Math.max(1, g.measureText(ch).width));
      g.save(); g.translate(w / 2, h / 2 + px * 0.36); g.scale(squeeze, 1); g.fillStyle = "#f4f0e4"; g.fillText(ch, 0, 0); g.restore();
      g.fillStyle = "#050506"; g.fillRect(0, h / 2 - 0.9, w, 1.8);
      g.fillStyle = "rgba(255,255,255,0.09)"; g.fillRect(0, h / 2 + 0.9, w, 0.8);
      g.fillStyle = "rgba(255,255,255,0.16)"; g.fillRect(0, 0, w, 0.9);
      g.fillStyle = "#050506"; g.fillRect(0, h / 2 - h * 0.055, w * 0.05, h * 0.11); g.fillRect(w * 0.95, h / 2 - h * 0.055, w * 0.05, h * 0.11);
      g.restore();
    };

    const build = () => {
      const box = cv.getBoundingClientRect();
      S = (Math.max(280, box.width) / W) * Math.min(2, window.devicePixelRatio || 1);
      cv.width = Math.round(W * S); cv.height = Math.round(H * S);
      atlas = SIZES.map(([cw, ch], kind) => {
        const drum = kind === 1 ? D_DRUM : L_DRUM, tw = Math.round(cw * S), th = Math.round((ch * S) / 2) * 2;
        const a = document.createElement("canvas"); a.width = tw * drum.length; a.height = th;
        const g = a.getContext("2d");
        if (g) for (let i = 0; i < drum.length; i++) { g.save(); g.translate(i * tw, 0); g.scale(tw / cw, th / ch); paintFlap(g, cw, ch, drum[i]); g.restore(); }
        return { a, tw, th };
      });

      // The housing never changes, so it is painted once.
      bg = document.createElement("canvas"); bg.width = cv.width; bg.height = cv.height;
      const g = bg.getContext("2d");
      if (!g) return;
      g.scale(S, S); g.translate(OX, 0);
      // Shadow on the wall behind the board.
      const OFF = 4000;
      g.save(); g.shadowColor = "rgba(48,40,26,0.4)"; g.shadowBlur = 34 * S; g.shadowOffsetX = OFF * S; g.shadowOffsetY = 34 * S; rr(g, 40 - OFF, 96, 680, 338, 14); g.fill(); g.restore();
      g.save(); g.shadowColor = "rgba(40,32,20,0.5)"; g.shadowBlur = 12 * S; g.shadowOffsetX = OFF * S; g.shadowOffsetY = 9 * S; rr(g, 24 - OFF, 74, 712, 360, 12); g.fill(); g.restore();
      // Two rods to the ceiling.
      for (const x of [150, 610]) {
        const rod = g.createLinearGradient(x - 5, 0, x + 5, 0); rod.addColorStop(0, "#3a3c40"); rod.addColorStop(0.35, "#b9bcc1"); rod.addColorStop(0.55, "#74777c"); rod.addColorStop(1, "#1e2023");
        g.fillStyle = rod; g.fillRect(x - 5, 0, 10, 72);
        const col = g.createLinearGradient(x - 12, 0, x + 12, 0); col.addColorStop(0, "#26282b"); col.addColorStop(0.35, "#8d9096"); col.addColorStop(1, "#141517");
        g.fillStyle = col; rr(g, x - 12, 60, 24, 12, 2); g.fill();
      }
      // Steel body with a brushed grain.
      rr(g, 20, 70, 720, 364, 12);
      const body = g.createLinearGradient(0, 70, 0, 434); body.addColorStop(0, "#3a3d41"); body.addColorStop(0.12, "#2b2d31"); body.addColorStop(1, "#1b1c1f");
      g.fillStyle = body; g.fill();
      g.save(); g.clip();
      let seed = 11; const rand = () => { seed = (seed * 16807) % 2147483647; return seed / 2147483647; };
      for (let y = 70; y < 434; y += 0.7) { const v = rand(); g.fillStyle = v > 0.5 ? `rgba(255,255,255,${(v - 0.5) * 0.07})` : `rgba(0,0,0,${(0.5 - v) * 0.16})`; g.fillRect(20 + rand() * 60, y, 720, 0.6); }
      const wash = g.createRadialGradient(380, 40, 20, 380, 70, 460); wash.addColorStop(0, "rgba(255,244,222,0.16)"); wash.addColorStop(1, "rgba(255,244,222,0)");
      g.fillStyle = wash; g.fillRect(20, 70, 720, 364);
      g.restore();
      g.lineWidth = 1; g.strokeStyle = "#0b0c0d"; rr(g, 20.5, 70.5, 719, 363, 12); g.stroke();
      const lip = g.createLinearGradient(0, 70, 0, 434); lip.addColorStop(0, "rgba(255,255,255,0.5)"); lip.addColorStop(0.08, "rgba(255,255,255,0.05)"); lip.addColorStop(0.9, "rgba(0,0,0,0)"); lip.addColorStop(1, "rgba(0,0,0,0.5)");
      g.strokeStyle = lip; g.lineWidth = 1.2; rr(g, 21.6, 71.6, 716.8, 360.8, 11); g.stroke();
      // The window the flaps sit in.
      rr(g, 38, 118, 684, 298, 7); g.fillStyle = "#08090a"; g.fill();
      g.save(); g.clip();
      const inner = g.createLinearGradient(0, 118, 0, 150); inner.addColorStop(0, "rgba(0,0,0,0.95)"); inner.addColorStop(1, "rgba(0,0,0,0)");
      g.fillStyle = "#101113"; g.fillRect(38, 118, 684, 298); g.fillStyle = inner; g.fillRect(38, 118, 684, 32);
      g.restore();
      g.lineWidth = 1; g.strokeStyle = "rgba(255,255,255,0.16)"; g.beginPath(); g.moveTo(44, 416.5); g.lineTo(716, 416.5); g.stroke();
      g.strokeStyle = "rgba(0,0,0,0.9)"; rr(g, 37.5, 117.5, 685, 299, 7); g.stroke();
      // Printed legends.
      g.textBaseline = "middle"; g.fillStyle = "rgba(240,236,224,0.86)";
      const spaced = (text: string, x: number, y: number, gap: number, align: number) => { let w = 0; for (const ch of text) w += g.measureText(ch).width + gap; let cx = x - (w - gap) * align; g.textAlign = "left"; for (const ch of text) { g.fillText(ch, cx, y); cx += g.measureText(ch).width + gap; } };
      g.font = '700 12px "Helvetica Neue", Helvetica, Arial, sans-serif'; spaced("DEPARTURES", 56, 95, 3.2, 0);
      g.fillStyle = "rgba(240,236,224,0.5)"; g.font = '600 10px "Helvetica Neue", Helvetica, Arial, sans-serif'; spaced("TRACK 2", 690, 95, 2.4, 1);
      g.fillStyle = "rgba(240,236,224,0.62)"; g.font = '600 9.5px "Helvetica Neue", Helvetica, Arial, sans-serif';
      ["DAYS", "HOURS", "MINUTES", "SECONDS"].forEach((label, i) => spaced(label, 51 + 72.5 + i * 171, 334, 2.6, 0.5));
      // Slot-head screws in the corners.
      for (const [x, y, a] of [[36, 86, 0.5], [724, 86, 2.2], [36, 422, 1.4], [724, 422, 0.2]]) {
        const sc = g.createRadialGradient(x - 1.5, y - 1.5, 0.5, x, y, 5.5); sc.addColorStop(0, "#d9dbde"); sc.addColorStop(0.6, "#7c7f85"); sc.addColorStop(1, "#2a2c2f");
        g.beginPath(); g.arc(x, y, 5, 0, Math.PI * 2); g.fillStyle = sc; g.fill(); g.lineWidth = 0.8; g.strokeStyle = "rgba(0,0,0,0.8)"; g.stroke();
        g.lineWidth = 1.3; g.strokeStyle = "rgba(10,10,12,0.85)"; g.beginPath(); g.moveTo(x - Math.cos(a) * 3.6, y - Math.sin(a) * 3.6); g.lineTo(x + Math.cos(a) * 3.6, y + Math.sin(a) * 3.6); g.stroke();
      }
      // Wells behind each flap, with the edges of the waiting cards showing above it.
      for (const c of cells) {
        const [cw, ch] = SIZES[c.kind];
        const x = c.x - OX;
        g.fillStyle = "#000"; rr(g, x - 1.5, c.y - 5, cw + 3, ch + 8, 4); g.fill();
        for (let k = 0; k < 3; k++) { g.fillStyle = `rgba(255,255,255,${0.1 - k * 0.028})`; g.fillRect(x + 1 + k, c.y - 1.6 - k * 1.3, cw - 2 - k * 2, 0.7); }
      }
    };

    const draw = (t: number) => {
      if (!bg) return;
      ctx.setTransform(1, 0, 0, 1, 0, 0);
      ctx.clearRect(0, 0, cv.width, cv.height);
      ctx.drawImage(bg, 0, 0);
      for (const c of cells) {
        const { a, tw, th } = atlas[c.kind], hh = th / 2, x = Math.round(c.x * S), y = Math.round(c.y * S);
        if (c.p < 0) { ctx.drawImage(a, c.shown * tw, 0, tw, th, x, y, tw, th); continue; }
        ctx.drawImage(a, c.next * tw, 0, tw, hh, x, y, tw, hh);
        ctx.drawImage(a, c.shown * tw, hh, tw, hh, x, y + hh, tw, hh);
        const e = c.p * c.p * (1.6 - 0.6 * c.p); // the card speeds up as it falls
        if (e < 0.5) {
          const k = Math.cos(e * Math.PI), fh = Math.max(1, hh * k);
          ctx.fillStyle = `rgba(0,0,0,${0.5 * (1 - k)})`; ctx.fillRect(x, y + hh, tw, hh * (1 - k) * 0.9);
          ctx.drawImage(a, c.shown * tw, 0, tw, hh, x - (1 - k) * S, y + hh - fh, tw + (1 - k) * 2 * S, fh);
          ctx.fillStyle = `rgba(255,255,255,${0.1 * (1 - k)})`; ctx.fillRect(x, y + hh - fh, tw, fh);
        } else {
          const k = -Math.cos(e * Math.PI), fh = Math.max(1, hh * k);
          ctx.drawImage(a, c.next * tw, hh, tw, hh, x - (1 - k) * S, y + hh, tw + (1 - k) * 2 * S, fh);
          ctx.fillStyle = `rgba(0,0,0,${0.55 * (1 - k)})`; ctx.fillRect(x, y + hh, tw, fh);
        }
      }
      ctx.setTransform(S, 0, 0, S, OX * S, 0);
      // The amber lamp that says the board is live.
      const on = reduce ? 1 : 0.6 + 0.4 * Math.sin(t / 520);
      ctx.beginPath(); ctx.arc(706, 95, 5.4, 0, Math.PI * 2); ctx.fillStyle = "#070708"; ctx.fill();
      ctx.save(); ctx.shadowColor = "#ffab2e"; ctx.shadowBlur = 16 * on * S; ctx.globalAlpha = 0.3 + 0.7 * on;
      ctx.beginPath(); ctx.arc(706, 95, 3.6, 0, Math.PI * 2); ctx.fillStyle = "#ffb13b"; ctx.fill(); ctx.restore();
      ctx.beginPath(); ctx.arc(704.8, 93.8, 1.1, 0, Math.PI * 2); ctx.fillStyle = "rgba(255,255,255,0.7)"; ctx.fill();
      // A pane of glass over the window.
      const glass = ctx.createLinearGradient(120, 118, 420, 416); glass.addColorStop(0, "rgba(255,255,255,0.075)"); glass.addColorStop(0.42, "rgba(255,255,255,0.02)"); glass.addColorStop(0.43, "rgba(255,255,255,0)"); glass.addColorStop(1, "rgba(255,255,255,0.03)");
      ctx.fillStyle = glass; rr(ctx, 38, 118, 684, 298, 7); ctx.fill();
    };

    let msg = 0, lastDigits = "", lastSec = -1, raf = 0, last = 0, t0 = 0, seen = true, nextMsg = 5200, touched = -1e9;
    const sync = (first: boolean) => {
      const leftMs = end - Date.now(), v = split(leftMs), digits = pad2(v.d) + pad2(v.h) + pad2(v.m) + pad2(v.s);
      if (digits !== lastDigits) { lastDigits = digits; nums.forEach((c, i) => aim(c, D_DRUM.indexOf(digits[i]), 0, first ? 160 + i * 46 : 0)); }
      const sec = Math.floor(Math.max(0, leftMs) / 1000);
      if (sec !== lastSec) { lastSec = sec; setInfo({ left: sec, when }); }
    };
    const step = (dt: number) => {
      for (const c of cells) {
        if (c.p < 0) {
          if (c.left <= 0) continue;
          if (c.wait > 0) { c.wait -= dt; continue; }
          c.next = mod(c.goal - (c.left - 1), c.n); c.p = 0; c.dur = c.left > 1 ? 52 + ((c.x * 7) % 16) : 150;
        }
        c.p += dt / c.dur;
        if (c.p >= 1) { c.shown = c.next; c.left -= 1; c.p = -1; }
      }
    };
    const frame = (now: number) => {
      raf = 0;
      if (!t0) { t0 = now; last = now; }
      const dt = Math.min(50, now - last); last = now;
      const t = now - t0;
      sync(false);
      if (t > nextMsg && now - touched > 2500) { msg = (msg + 1) % messages.length; write(top, centre(messages[msg], 12), 5, 34, 0); nextMsg = t + 5200; }
      step(dt); draw(t);
      if (seen && !document.hidden) raf = requestAnimationFrame(frame);
    };
    const wake = () => { if (!raf && !reduce) { last = performance.now(); raf = requestAnimationFrame(frame); } };

    build();
    write(top, centre(messages[0], 12), 6, 26, 40);
    write(date, centre(dateLine, 16), 5, 20, 330);
    sync(true);
    let timer = 0;
    if (reduce) { draw(0); timer = window.setInterval(() => { sync(false); draw(0); }, 1000); } else wake();

    const at = (e: PointerEvent) => { const b = cv.getBoundingClientRect(); return [((e.clientX - b.left) / b.width) * W, ((e.clientY - b.top) / b.height) * H]; };
    const onMove = (e: PointerEvent) => {
      if (reduce) return;
      const [px, py] = at(e), now = performance.now();
      for (const c of cells) {
        if (c.kind === 1 || c.left > 0 || now - c.kicked < 900) continue;
        const [cw, ch] = SIZES[c.kind];
        if (Math.hypot(c.x + cw / 2 - px, c.y + ch / 2 - py) < 46) { c.kicked = now; touched = now; aim(c, c.goal, 4, 0); }
      }
    };
    api.current = {
      shuffle: (px, py) => {
        touched = performance.now();
        for (const c of cells) { const [cw, ch] = SIZES[c.kind]; if (c.left <= 0) aim(c, c.goal, c.kind === 1 ? 1 : 7, Math.hypot(c.x + cw / 2 - px, c.y + ch / 2 - py) * 1.1); }
        wake();
      },
    };
    const onDown = (e: PointerEvent) => { const [px, py] = at(e); api.current?.shuffle(px, py); };
    cv.addEventListener("pointermove", onMove);
    cv.addEventListener("pointerdown", onDown);
    const ro = new ResizeObserver(() => { build(); if (reduce) draw(0); });
    ro.observe(cv);
    const io = new IntersectionObserver(([entry]) => { seen = entry.isIntersecting; if (seen) wake(); });
    io.observe(cv);
    const onVis = () => { if (!document.hidden) wake(); };
    document.addEventListener("visibilitychange", onVis);
    return () => {
      cancelAnimationFrame(raf); window.clearInterval(timer); api.current = null;
      cv.removeEventListener("pointermove", onMove); cv.removeEventListener("pointerdown", onDown);
      ro.disconnect(); io.disconnect(); document.removeEventListener("visibilitychange", onVis);
    };
  }, []);

  const v = info ? split(info.left * 1000) : null;
  return (
    <div className="flex min-h-[520px] w-full flex-col items-center justify-start bg-[#d9d5cb] bg-[radial-gradient(90%_70%_at_50%_0%,#ebe8e0_0%,#d9d5cb_55%,#c9c4b8_100%)] px-4 pb-12 sm:min-h-[760px] sm:px-8">
      <button
        type="button"
        aria-label="Shuffle the departure board"
        onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); api.current?.shuffle(430, 250); } }}
        className="block w-full max-w-[1010px] cursor-pointer touch-manipulation rounded-2xl outline-none focus-visible:ring-2 focus-visible:ring-stone-900 focus-visible:ring-offset-4 focus-visible:ring-offset-[#d9d5cb]"
      >
        <canvas ref={canvas} aria-hidden="true" className="block aspect-[860/560] w-full" />
      </button>
      <p role="timer" className="-mt-3 min-h-[20px] text-center font-mono text-[11px] uppercase leading-5 tracking-[0.16em] text-stone-600 sm:-mt-12">
        <span className="text-stone-900">{EVENT} leaves {info ? info.when : "soon"}</span>
        <span className="mx-2 hidden text-stone-400 sm:inline">/</span>
        <span className="block tabular-nums sm:inline">{v ? (info && info.left === 0 ? "Departed" : `${v.d} days ${pad2(v.h)} h ${pad2(v.m)} min ${pad2(v.s)} s`) : "Counting"}</span>
      </p>
      <a href="#" className="mt-5 inline-flex h-11 items-center rounded-full bg-stone-900 px-6 text-sm font-medium text-stone-50 shadow-[0_1px_0_rgba(255,255,255,0.25)_inset,0_10px_24px_-10px_rgba(40,30,10,0.6)] outline-none transition-transform duration-200 hover:-translate-y-0.5 focus-visible:ring-2 focus-visible:ring-stone-900 focus-visible:ring-offset-2 focus-visible:ring-offset-[#d9d5cb] motion-reduce:transition-none motion-reduce:hover:translate-y-0">
        Reserve a seat
      </a>
    </div>
  );
}
```
