# Stat Reel

> An endless wheel of labels glides past a fixed center line while a giant gradient-filled figure swaps in beside it; scroll to seize the reel and it resumes on its own after four seconds.

A shadcn/ui block from the [@uicapsule](https://uicapsule.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/uicapsule/stat-reel).

- Page, with a live preview: https://shadcn.exchange/r/uicapsule/stat-reel
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/uicapsule/stat-reel-light-155e8da5f6e1ec06.png
- Registry item: https://uicapsule.com/r/stat-reel.json
- Categories: data, minimal
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://uicapsule.com/r/stat-reel.json
```

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

## Dependencies

- npm packages: `gsap`

## Files

### `/preview.tsx`

```tsx
"use client";

import { StatReel } from "./stat-reel";

const Preview = () => <StatReel />;

export default Preview;
```

### `/stat-reel-data.ts`

```ts
/**
 * Static dataset for the stat reel.
 *
 * The gradient and the stat are declared together so the 1:1 pairing between a
 * figure and its colour triad cannot drift apart: there is no second array to
 * keep in sync.
 */

const GRAD_SIZE = "95% 95%";
const GRAD_POS = { x: 50, y: 110 } as const;
const GRAD_BASE = "#2a2e36";

type Triad = readonly [string, string, string];

const gradientFrom = ([c0, c1, c2]: Triad) =>
  `radial-gradient(${GRAD_SIZE} at ${GRAD_POS.x}% ${GRAD_POS.y}%, ${c0} 0%, rgba(${c1},0.669) 34.13%, rgba(${c2},0.5) 65.38%, rgba(158,144,96,0) 100%), linear-gradient(${GRAD_BASE}, ${GRAD_BASE})`;

interface RawStat {
  readonly prefix?: string;
  readonly value: string;
  readonly label: string;
  readonly triad: Triad;
}

export interface Stat {
  readonly prefix?: string;
  readonly value: string;
  readonly label: string;
  /** Ready-to-paint `background-image` for the transparent-text figure. */
  readonly gradient: string;
}

const RAW = [
  {
    label: "Depth of the Challenger Deep",
    prefix: "Down to",
    triad: ["#d7be68", "215,50,154", "84,109,208"],
    value: "10,935m",
  },
  {
    label: "Pressure at the trench floor",
    triad: ["#78ec81", "50,215,193", "208,84,177"],
    value: "1,100×",
  },
  {
    label: "Water at two kilometres down",
    triad: ["#ecbf78", "215,50,66", "84,88,208"],
    value: "4°C",
  },
  {
    label: "Light in the midnight zone",
    triad: ["#78c8ec", "120,84,236", "236,84,140"],
    value: "0%",
  },
  {
    label: "Of seafloor mapped in detail",
    prefix: "Under",
    triad: ["#c8ec78", "50,200,120", "90,84,208"],
    value: "1%",
  },
  {
    label: "Top of the twilight zone",
    triad: ["#ecae78", "236,130,50", "150,84,208"],
    value: "200m",
  },
  {
    label: "Of deep-sea life that glows",
    triad: ["#78ece4", "84,140,236", "208,84,200"],
    value: "76%",
  },
  {
    label: "Round trip for a crewed dive",
    triad: ["#ec78b4", "236,84,150", "84,120,208"],
    value: "11h",
  },
  {
    label: "New marine species each year",
    prefix: "Over",
    triad: ["#ece078", "150,215,50", "50,200,193"],
    value: "2,000",
  },
  {
    label: "Average depth of the ocean",
    triad: ["#b478ec", "208,84,200", "236,180,84"],
    value: "3.7km",
  },
  {
    label: "Of Earth's water is ocean",
    triad: ["#78ecc8", "50,200,215", "120,84,236"],
    value: "97%",
  },
  {
    label: "Speed of sound in seawater",
    triad: ["#ec8878", "236,84,84", "150,84,236"],
    value: "1,500m/s",
  },
  {
    label: "Abyssal sediment per 1,000yr",
    triad: ["#9aecb0", "50,215,180", "84,120,236"],
    value: "1cm",
  },
  {
    label: "Lifespan of Greenland shark",
    prefix: "Up to",
    triad: ["#a8a8ec", "84,120,236", "236,84,180"],
    value: "400yr",
  },
  {
    label: "Deepest fish ever filmed",
    triad: ["#ecc878", "236,84,180", "50,200,200"],
    value: "8,336m",
  },
  {
    label: "Freezing point of seawater",
    triad: ["#a8ec78", "150,215,50", "84,100,236"],
    value: "−1.8°C",
  },
  {
    label: "Known hydrothermal vents",
    prefix: "Over",
    triad: ["#ec9ab0", "236,84,140", "100,84,236"],
    value: "600",
  },
  {
    label: "Hottest water microbes take",
    triad: ["#78ecd0", "50,215,140", "215,84,200"],
    value: "121°C",
  },
  {
    label: "Added pressure per 10 metres",
    triad: ["#ecd278", "236,160,50", "84,110,236"],
    value: "1atm",
  },
  {
    label: "Between two high tides",
    triad: ["#90a8ec", "140,84,236", "236,90,150"],
    value: "12h25m",
  },
  {
    label: "Carbon absorbed each year",
    prefix: "Roughly",
    triad: ["#78ecb8", "50,200,200", "160,84,236"],
    value: "2.5Gt",
  },
  {
    label: "Where the hadal zone begins",
    triad: ["#eca090", "236,110,84", "84,130,236"],
    value: "6,000m",
  },
  {
    label: "Of your oxygen from plankton",
    prefix: "Nearly",
    triad: ["#d0ec78", "80,210,110", "215,84,190"],
    value: "50%",
  },
  {
    label: "One global conveyor cycle",
    triad: ["#9ad8ec", "84,150,236", "215,84,180"],
    value: "1,000yr",
  },
] as const satisfies readonly [RawStat, ...RawStat[]];

const toStat = (raw: RawStat): Stat => ({
  gradient: gradientFrom(raw.triad),
  label: raw.label,
  prefix: raw.prefix,
  value: raw.value,
});

const [FIRST, ...REST] = RAW;

/**
 * Non-empty by construction, so `STATS[0]` is a safe fallback under
 * `noUncheckedIndexedAccess`.
 */
export const STATS: readonly [Stat, ...Stat[]] = [toStat(FIRST), ...REST.map(toStat)];

export const N = STATS.length;

/** Wraps any integer (including negatives) into a defined stat. */
export const statAt = (index: number): Stat => {
  const wrapped = ((Math.trunc(index) % N) + N) % N;
  return STATS[wrapped] ?? STATS[0];
};
```

### `/stat-reel.tsx`

```tsx
"use client";

import type { CSSProperties, FC } from "react";
import { memo, useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
import { gsap } from "gsap";

import { N, STATS, statAt } from "./stat-reel-data";

const DESKTOP = { font: 26, itemH: 62 };
const MOBILE = { font: 19, itemH: 46 };
/**
 * Gates the first auto-step behind the entrance timeline. Floor is ~250ms:
 * below that the per-focus figure pop (overwrite: true) fires while the
 * entrance still owns the figure and kills the cold open.
 */
const START_DELAY_MS = 600;
const BREAKPOINT = 900;
const TOUCH_GAIN = 1.2;
const ROW_COUNT = N * 2;

/**
 * Wheel deltas arrive in three units (`WheelEvent.deltaMode`): pixels (0),
 * lines (1) and pages (2). Chrome and Safari only ever report pixels, but
 * Firefox on Windows/Linux reports lines, so the accumulator has to convert
 * to pixels before comparing against `CFG.wheelStep`. This nominal line
 * height only has to be in the right order of magnitude.
 */
const WHEEL_LINE_PX = 16;

/**
 * A wheel or touch gesture delivers events milliseconds apart, so a gap this
 * long means the gesture ended and any sub-threshold residue in the
 * accumulator is stale. Without expiry, 44px of ignored input waits
 * indefinitely for an unrelated flick minutes later to top it over
 * `CFG.wheelStep` and steal a row. Not a feel constant: every value well above
 * a frame and well below human re-engagement behaves identically for real
 * gestures.
 */
const GESTURE_IDLE_MS = 500;

const COLOR_REST = "#636365";
const COLOR_FOCUS = "#010102";

/**
 * Declared locally: the host app never defines `--font-sans`, so Tailwind's
 * `font-sans` resolves to a cycle and falls back to Times.
 */
const SANS =
  'ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif';

const CFG = {
  centerScale: 1.68,
  dwell: 0.6,
  falloff: 270,
  falloffMobile: 150,
  farScale: 0.7,
  manualStep: 1.2,
  numScale: 1.05,
  resume: 4,
  step: 0.8,
  wheelStep: 45,
};

/** Scale a fully-defocused row shrinks to, relative to the focused row. */
const FAR_RATIO = CFG.farScale / CFG.centerScale;

const clamp01 = (v: number) => (v < 0 ? 0 : Math.min(1, v));
const lerp = (a: number, b: number, t: number) => a + (b - a) * t;
const smoothstep = (t: number) => t * t * (3 - 2 * t);

const useIsoLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;

/** `CSSProperties` widened to accept custom properties without a type assertion. */
interface CSSVarProperties extends CSSProperties {
  [key: `--${string}`]: string | number | undefined;
}

const PANEL_MASK =
  "linear-gradient(to bottom, transparent 0%, #000 16%, #000 84%, transparent 100%)";

const SECTION_STYLE: CSSProperties = {
  fontFamily: SANS,
};

const PANEL_STYLE: CSSProperties = {
  WebkitMaskImage: PANEL_MASK,
  maskImage: PANEL_MASK,
};

const ROW_STYLE: CSSProperties = {
  fontSize: "calc(var(--bm-base-font) * var(--center-scale))",
  height: "var(--bm-item-h)",
};

/** Breathing room kept between the widest figure and the column's inner edge. */
const FIT_GUTTER_PX = 10;

/** Smallest fraction of its natural size the figure may be shrunk to. */
const FIT_FLOOR = 0.4;

/**
 * Shrinks the figure until it fits the left column, down to a 40% floor.
 * Writes `--bm-num-fit`, which the font-size `calc()` multiplies in.
 */
const applyFit = (num: HTMLElement, col: HTMLElement) => {
  num.style.setProperty("--bm-num-fit", "1");
  const natural = num.scrollWidth;
  const cs = window.getComputedStyle(col);
  // oxlint-disable-next-line unicorn/prefer-number-coercion -- computed padding carries a px unit, which Number() rejects
  const padL = Number.parseFloat(cs.paddingLeft) || 0;
  // oxlint-disable-next-line unicorn/prefer-number-coercion -- computed padding carries a px unit, which Number() rejects
  const padR = Number.parseFloat(cs.paddingRight) || 0;
  const available = col.clientWidth - padL - padR - FIT_GUTTER_PX;
  // A 0-wide column (hidden panel, freshly-mounted iframe) makes `available`
  // negative, which the 0.4 floor would silently absorb into a real-looking
  // shrink. Keep the `1` written above until there is something to fit into.
  if (available <= 0) {
    return;
  }
  if (natural > 0 && natural > available) {
    num.style.setProperty("--bm-num-fit", String(Math.max(FIT_FLOOR, available / natural)));
  }
};

interface RowsProps {
  register: (index: number, el: HTMLDivElement | null) => void;
}

const RowsBase: FC<RowsProps> = ({ register }) => (
  <>
    {Array.from({ length: ROW_COUNT }, (_, j) => (
      <div
        key={j}
        ref={(el) => register(j, el)}
        style={ROW_STYLE}
        className="pointer-events-none absolute top-0 right-0 left-[clamp(32px,5vw,80px)] flex origin-left items-center leading-[1.05] font-medium whitespace-nowrap text-[#636365] [will-change:transform]"
      >
        {statAt(j).label}
      </div>
    ))}
  </>
);

const Rows = memo(RowsBase);

Rows.displayName = "StatReelRows";

export const StatReel: FC = () => {
  const [focusedIndex, setFocusedIndex] = useState(0);

  const sectionRef = useRef<HTMLElement>(null);
  const leftRef = useRef<HTMLDivElement>(null);
  const figureRef = useRef<HTMLDivElement>(null);
  const numberRef = useRef<HTMLSpanElement>(null);
  const panelRef = useRef<HTMLDivElement>(null);
  const rowsRef = useRef<(HTMLDivElement | null)[]>([]);

  /** The single scalar every tween drives; the rAF loop paints from it. */
  const proxy = useRef({ idx: 0 });
  const targetRef = useRef(0);
  const modeRef = useRef<"auto" | "user">("auto");
  const focusedRef = useRef(0);
  const accumRef = useRef(0);
  const reducedRef = useRef(false);
  const firstFigure = useRef(true);
  const sizeRef = useRef({
    cy: 0,
    falloff: CFG.falloff,
    itemH: DESKTOP.itemH,
    panelH: 0,
  });

  const register = useCallback((index: number, el: HTMLDivElement | null) => {
    rowsRef.current[index] = el;
  }, []);

  useEffect(() => {
    const section = sectionRef.current;
    const figure = figureRef.current;
    const panel = panelRef.current;

    if (!section || !figure || !panel) {
      return;
    }

    let disposed = false;
    const proxyState = proxy.current;

    const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
    reducedRef.current = mq.matches;

    const onMq = (e: MediaQueryListEvent) => {
      reducedRef.current = e.matches;
    };

    mq.addEventListener("change", onMq);

    const isReduced = () => reducedRef.current;

    const measure = () => {
      const mobile = window.innerWidth < BREAKPOINT;
      const base = mobile ? MOBILE : DESKTOP;
      const panelH = panel.clientHeight;

      sizeRef.current = {
        cy: panelH / 2,
        falloff: mobile ? CFG.falloffMobile : CFG.falloff,
        itemH: base.itemH,
        panelH,
      };

      section.style.setProperty("--bm-item-h", `${base.itemH}px`);
      section.style.setProperty("--bm-base-font", `${base.font}px`);
      section.style.setProperty("--center-scale", String(CFG.centerScale));

      if (numberRef.current && leftRef.current) {
        applyFit(numberRef.current, leftRef.current);
      }
    };

    measure();
    window.addEventListener("resize", measure);

    // The gallery resizes the frame without ever firing a window resize.
    const ro = new ResizeObserver(() => {
      // A freshly-mounted iframe reports 0x0 on the first callback.
      if (section.clientWidth === 0 || section.clientHeight === 0) {
        return;
      }
      measure();
    });
    ro.observe(section);

    // Fonts swap in after first paint; the initial applyFit measured fallbacks.
    const refitAfterFonts = async () => {
      await document.fonts.ready;
      if (!disposed) {
        measure();
      }
    };

    void refitAfterFonts();

    let dwellTimer: ReturnType<typeof setTimeout> | null = null;
    let resumeTimer: ReturnType<typeof setTimeout> | null = null;
    let startTimer: ReturnType<typeof setTimeout> | null = null;

    const tweenTo = (t: number, duration: number, ease: string, onComplete: () => void) => {
      gsap.to(proxyState, {
        duration: isReduced() ? 0 : duration,
        ease,
        idx: t,
        onComplete,
        overwrite: true,
      });
    };

    /**
     * In auto mode, queues the next advance one dwell from now; every tween
     * hands back here on completion, so the reel keeps stepping until the
     * mode changes.
     */
    const queueAutoStep = () => {
      if (modeRef.current !== "auto" || isReduced()) {
        return;
      }
      dwellTimer = setTimeout(() => {
        targetRef.current += 1;
        tweenTo(targetRef.current, CFG.step, "expo.inOut", queueAutoStep);
      }, CFG.dwell * 1000);
    };

    const startAuto = () => {
      if (isReduced()) {
        return;
      }

      modeRef.current = "auto";
      targetRef.current = Math.round(proxyState.idx);

      if (dwellTimer) {
        clearTimeout(dwellTimer);
      }
      queueAutoStep();
    };

    const stepManual = (dir: number) => {
      if (startTimer) {
        clearTimeout(startTimer);
      }
      if (dwellTimer) {
        clearTimeout(dwellTimer);
      }
      if (resumeTimer) {
        clearTimeout(resumeTimer);
      }

      modeRef.current = "user";
      targetRef.current += dir;
      tweenTo(targetRef.current, CFG.manualStep, "expo.out", queueAutoStep);

      resumeTimer = setTimeout(startAuto, CFG.resume * 1000);
    };

    /** Input accumulates continuously but only advances in whole rows. */
    const drain = () => {
      while (Math.abs(accumRef.current) >= CFG.wheelStep) {
        const d = Math.sign(accumRef.current);
        accumRef.current -= d * CFG.wheelStep;
        stepManual(d);
      }
    };

    let idleTimer: ReturnType<typeof setTimeout> | null = null;

    /**
     * The single entry point for wheel and touch alike: both feed pixels in,
     * whole rows come out, and the remainder is dropped once the gesture ends.
     */
    const accumulate = (deltaPx: number) => {
      accumRef.current += deltaPx;
      drain();

      if (idleTimer) {
        clearTimeout(idleTimer);
      }
      idleTimer = setTimeout(() => {
        accumRef.current = 0;
      }, GESTURE_IDLE_MS);
    };

    let rafId = 0;
    let prevNearest: HTMLDivElement | null = null;

    const render = () => {
      const { itemH, falloff, panelH, cy } = sizeRef.current;

      // Skip painting until the panel has a real height, or every row would
      // land offscreen for a frame inside a freshly-mounted iframe.
      if (panelH > 0) {
        const { idx } = proxyState;
        const rows = rowsRef.current;
        const windowH = ROW_COUNT * itemH;
        const halfWin = windowH / 2;

        let nearestEl: HTMLDivElement | null = null;
        let nearestAbs = Infinity;

        for (let j = 0; j < ROW_COUNT; j += 1) {
          const row = rows[j];
          if (!row) {
            continue;
          }

          let off = (j - idx) * itemH;
          // Wrap into (-halfWin, halfWin] so the reel never ends.
          off = ((((off + halfWin) % windowH) + windowH) % windowH) - halfWin;
          const screenY = cy + off;

          if (screenY < -itemH || screenY > panelH + itemH) {
            if (row.style.opacity !== "0") {
              row.style.opacity = "0";
            }
            continue;
          }

          const s = smoothstep(clamp01(Math.abs(off) / falloff));
          if (row.style.opacity !== "1") {
            row.style.opacity = "1";
          }
          row.style.transform = `translateY(${screenY - itemH / 2}px) scale(${lerp(1, FAR_RATIO, s)})`;

          const a = Math.abs(off);
          if (a < nearestAbs) {
            nearestAbs = a;
            nearestEl = row;
          }
        }

        // Only touch color when the centre row actually changes.
        if (nearestEl !== prevNearest) {
          if (prevNearest) {
            prevNearest.style.color = COLOR_REST;
          }
          if (nearestEl) {
            nearestEl.style.color = COLOR_FOCUS;
          }
          prevNearest = nearestEl;
        }

        const focused = ((Math.round(idx) % N) + N) % N;
        if (focused !== focusedRef.current) {
          focusedRef.current = focused;
          setFocusedIndex(focused);
        }
      }

      rafId = requestAnimationFrame(render);
    };

    rafId = requestAnimationFrame(render);

    const onWheel = (e: WheelEvent) => {
      e.preventDefault();
      let unit = 1;
      if (e.deltaMode === 1) {
        unit = WHEEL_LINE_PX;
      } else if (e.deltaMode === 2) {
        unit = sizeRef.current.panelH;
      }
      accumulate(e.deltaY * unit);
    };

    /**
     * The reel tracks exactly one finger, latched by identifier. Reading
     * `touches[0]` instead would silently re-anchor onto a different contact
     * the moment the tracked finger lifts out of a multi-touch gesture,
     * turning the next 2px move into a jump of a dozen rows.
     */
    let activeTouch: number | null = null;
    let touchY = 0;

    const findTouch = (list: TouchList, id: number): Touch | null => {
      for (const t of list) {
        if (t.identifier === id) {
          return t;
        }
      }
      return null;
    };

    const onTouchStart = (e: TouchEvent) => {
      if (activeTouch !== null) {
        return;
      }

      const [t] = e.changedTouches;
      if (!t) {
        return;
      }

      activeTouch = t.identifier;
      touchY = t.clientY;
    };

    const onTouchMove = (e: TouchEvent) => {
      if (activeTouch === null) {
        return;
      }

      const t = findTouch(e.touches, activeTouch);
      if (!t) {
        return;
      }

      const dy = touchY - t.clientY;
      touchY = t.clientY;
      e.preventDefault();
      accumulate(dy * TOUCH_GAIN);
    };

    const onTouchEnd = (e: TouchEvent) => {
      if (activeTouch === null) {
        return;
      }
      if (!findTouch(e.changedTouches, activeTouch)) {
        return;
      }

      // Hand tracking to a finger that is still down rather than going dead
      // until the next touchstart. Re-anchoring `touchY` to its current
      // position is what keeps the handover from registering as a jump.
      const [next] = e.touches;
      activeTouch = next ? next.identifier : null;
      if (next) {
        touchY = next.clientY;
      }
    };

    const onKeyDown = (e: KeyboardEvent) => {
      let dir = 0;
      if (e.key === "ArrowDown") {
        dir = 1;
      } else if (e.key === "ArrowUp") {
        dir = -1;
      }
      if (dir === 0) {
        return;
      }

      e.preventDefault();
      stepManual(dir);
    };

    section.addEventListener("wheel", onWheel, { passive: false });
    section.addEventListener("touchstart", onTouchStart, { passive: true });
    section.addEventListener("touchmove", onTouchMove, { passive: false });
    section.addEventListener("touchend", onTouchEnd, { passive: true });
    section.addEventListener("touchcancel", onTouchEnd, { passive: true });
    section.addEventListener("keydown", onKeyDown);

    const groups = [figure, panel];
    let intro: gsap.core.Timeline | null = null;

    if (isReduced()) {
      gsap.set(groups, { opacity: 1, scale: 1, y: 0 });
      firstFigure.current = false;
    } else {
      const tl = gsap.timeline({
        delay: 0.12,
        onComplete: () => {
          firstFigure.current = false;
        },
      });

      tl.fromTo(
        groups,
        { opacity: 0, scale: 0.96, y: 26 },
        {
          duration: 1,
          ease: "power4.out",
          opacity: 1,
          scale: 1,
          stagger: 0.12,
          y: 0,
        },
      );

      intro = tl;
      startTimer = setTimeout(startAuto, START_DELAY_MS);
    }

    return () => {
      disposed = true;
      cancelAnimationFrame(rafId);
      ro.disconnect();
      window.removeEventListener("resize", measure);
      mq.removeEventListener("change", onMq);
      section.removeEventListener("wheel", onWheel);
      section.removeEventListener("touchstart", onTouchStart);
      section.removeEventListener("touchmove", onTouchMove);
      section.removeEventListener("touchend", onTouchEnd);
      section.removeEventListener("touchcancel", onTouchEnd);
      section.removeEventListener("keydown", onKeyDown);

      if (dwellTimer) {
        clearTimeout(dwellTimer);
      }
      if (resumeTimer) {
        clearTimeout(resumeTimer);
      }
      if (startTimer) {
        clearTimeout(startTimer);
      }
      if (idleTimer) {
        clearTimeout(idleTimer);
      }

      gsap.killTweensOf(proxyState);
      gsap.killTweensOf(groups);
      // Killing only the child tween would leave the timeline on the ticker,
      // where it completes and fires `onComplete` after the reset below.
      intro?.kill();

      // StrictMode remounts: every latch must return to its mount-time value
      // or the second mount skips the cold open.
      firstFigure.current = true;
      modeRef.current = "auto";
      proxyState.idx = 0;
      targetRef.current = 0;
      focusedRef.current = 0;
      accumRef.current = 0;
    };
  }, []);

  // Both halves of a focus change: pop the incoming figure in, then refit it to
  // the left column now that its digits — and so its natural width — differ.
  // The entrance timeline owns the figure until it completes, so the pop stays
  // latched off for the cold open while the fit still has to run.
  useIsoLayoutEffect(() => {
    const figure = figureRef.current;

    if (figure && !firstFigure.current) {
      if (reducedRef.current) {
        gsap.set(figure, { opacity: 1, scale: 1, y: 0 });
      } else {
        gsap.fromTo(
          figure,
          { opacity: 0, scale: 0.95, y: 14 },
          {
            duration: Math.max(0.32, CFG.step * 0.7),
            ease: "expo.out",
            opacity: 1,
            overwrite: true,
            scale: 1,
            y: 0,
          },
        );
      }
    }

    if (numberRef.current && leftRef.current) {
      applyFit(numberRef.current, leftRef.current);
    }
  }, [focusedIndex]);

  const current = statAt(focusedIndex);

  const numberStyle: CSSVarProperties = {
    "--bm-num-scale": String(CFG.numScale),
    backgroundImage: current.gradient,
  };

  return (
    <section
      className="relative flex h-full w-full flex-col overflow-hidden bg-[#f2f2f4] text-[#010102] [--bm-base-font:19px] [--bm-item-h:46px] [--center-scale:1.68] min-[900px]:flex-row min-[900px]:[--bm-base-font:26px] min-[900px]:[--bm-item-h:62px]"
      style={SECTION_STYLE}
      ref={sectionRef}
      aria-label="Ocean depth statistics"
      // oxlint-disable-next-line jsx-a11y/no-noninteractive-tabindex -- wheel and touch already seize the reel; the tab stop is what gives a keyboard-only user the same arrow-key access to it
      tabIndex={0}
    >
      <div
        className="z-[1] flex min-w-0 shrink-0 grow-0 basis-auto flex-col items-center justify-center pt-[clamp(28px,7vw,56px)] pr-4 pb-0 pl-4 text-center min-[900px]:basis-[38%] min-[900px]:items-start min-[900px]:pt-0 min-[900px]:pr-[clamp(12px,1.4vw,28px)] min-[900px]:pl-[clamp(20px,2.6vw,52px)] min-[900px]:text-left"
        ref={leftRef}
        aria-hidden
      >
        <div className="opacity-0 [will-change:transform,opacity]" ref={figureRef}>
          <div className="mb-1.5 min-h-[26px] w-full text-center text-[clamp(14px,1.4vw,20px)] font-medium text-[#636365] min-[900px]:w-auto min-[900px]:text-left">
            {current.prefix ?? " "}
          </div>

          <span
            className="block bg-clip-text bg-no-repeat text-[calc(clamp(68px,20vw,124px)*var(--bm-num-scale,1.05)*var(--bm-num-fit,1))] leading-none font-semibold tracking-[-0.055em] whitespace-nowrap text-transparent [-webkit-background-clip:text] [-webkit-text-fill-color:transparent] min-[900px]:text-[calc(clamp(56px,9vw,152px)*var(--bm-num-scale,1.05)*var(--bm-num-fit,1))]"
            ref={numberRef}
            style={numberStyle}
          >
            {current.value}
          </span>
        </div>
      </div>

      <div
        className="relative z-[1] min-w-0 shrink grow basis-auto overflow-hidden px-[clamp(32px,5vw,80px)] opacity-0"
        ref={panelRef}
        style={PANEL_STYLE}
        aria-hidden
      >
        <Rows register={register} />
      </div>

      <ul className="sr-only">
        {STATS.map((stat) => (
          <li key={stat.label}>
            {`${stat.prefix ? `${stat.prefix} ` : ""}${stat.value} — ${stat.label}`}
          </li>
        ))}
      </ul>
    </section>
  );
};
```
