# Effort Picker

> Remake of the Codex effort picker, in ChatGPT's chrome or Claude's.

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

- Page, with a live preview: https://shadcn.exchange/r/uicapsule/effort-picker
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/uicapsule/effort-picker-light-ba0f30b537148d39.png
- Registry item: https://uicapsule.com/r/effort-picker.json
- Categories: controls, minimal, silly
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://uicapsule.com/r/effort-picker.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: `@mediapipe/tasks-vision`, `lucide-react`, `motion`

## Files

### `/composer-chrome.tsx`

```tsx
"use client";

import { motion } from "motion/react";
import {
  ArrowUpIcon,
  ChevronDownIcon,
  CircleCheckIcon,
  CornerDownLeftIcon,
  FolderIcon,
  GitBranchIcon,
  LaptopIcon,
  MicIcon,
  PlusIcon,
} from "lucide-react";

import type { ReactNode } from "react";
import type { MotionValue } from "motion/react";

import type { EffortTheme } from "./effort-theme";

import { LevelLabel } from "./effort-dial";
import { CARD_WIDTH } from "./effort-scale";
import { EFFORT_THEMES } from "./effort-theme";

/** ChatGPT's chip has two sizes. Its right edge is pinned, so opening grows it
 * leftward until it lines up under the card. Claude's chip doesn't grow — the
 * card just arrives above it. */
const CHIP_CLOSED_WIDTH = 200;
const CHIP_MORPH = { damping: 36, stiffness: 420, type: "spring" } as const;

/** The cost estimate doesn't move with the dial — only the level beside it does. */
const STATIC_SOL = "5.6 Sol";
const MODEL_NAME = "Fable 5";

interface ComposerChromeProps {
  /** Knob offset in px. The effort chip reads whatever the dial settles on. */
  knobX: MotionValue<number>;
  theme: EffortTheme;
  /** The chip is the one live control down here: it opens and closes the card. */
  onToggle: () => void;
  open: boolean;
  /** The card, anchored to the chip's right edge so the two line up. */
  popover: ReactNode;
}

/**
 * The prompt box never moves — the trick is in the chip: opening the card widens
 * it leftward from a compact pill into a bar the width of the card above it.
 * Everything here is scenery except the chip.
 */
const CodexComposer = ({ knobX, theme, onToggle, open, popover }: ComposerChromeProps) => {
  const tokens = EFFORT_THEMES[theme];

  return (
    <div className="select-none">
      <div aria-hidden className="flex items-center gap-7 px-4 pb-4 text-[17px] text-neutral-300">
        <span className="flex items-center gap-2.5">
          <FolderIcon className={`size-5 ${tokens.muted}`} />
          loremllm
        </span>
        <span className="flex items-center gap-2.5">
          <LaptopIcon className={`size-5 ${tokens.muted}`} />
          Local
        </span>
        <span className="flex items-center gap-2.5">
          <GitBranchIcon className={`size-5 ${tokens.muted}`} />
          main
        </span>
      </div>

      <div className="rounded-[26px] bg-neutral-800 px-5 pt-5 pb-4">
        <p aria-hidden className={`mb-10 text-[17px] ${tokens.faint}`}>
          Do anything
        </p>

        <div className="flex items-center gap-4">
          <PlusIcon aria-hidden className={`size-6 shrink-0 ${tokens.muted}`} />
          <span aria-hidden className={`flex items-center gap-2.5 text-[17px] ${tokens.muted}`}>
            <CircleCheckIcon className="size-5" />
            Approve for me
          </span>

          <div className="ml-auto flex items-center gap-3">
            <div className="relative" data-effort-control>
              {popover}

              <motion.button
                type="button"
                onClick={onToggle}
                aria-expanded={open}
                animate={{ width: open ? CARD_WIDTH : CHIP_CLOSED_WIDTH }}
                transition={CHIP_MORPH}
                // Closed, the chip is bare text — it only takes on a surface once it
                // widens into the bar under the card.
                className={`flex h-12 cursor-pointer items-center rounded-full px-5 text-[17px] outline-none focus-visible:ring-2 ${
                  tokens.ring
                } ${open ? "bg-neutral-700/60" : "hover:bg-white/5"}`}
              >
                <span className="flex flex-1 items-center justify-center gap-1.5 truncate">
                  <span className={tokens.text}>{STATIC_SOL}</span>
                  <LevelLabel knobX={knobX} theme={theme} className={tokens.muted} />
                </span>
                <ChevronDownIcon
                  className={`size-5 shrink-0 transition-transform duration-200 ${tokens.muted} ${
                    open ? "rotate-180" : ""
                  }`}
                />
              </motion.button>
            </div>

            <MicIcon aria-hidden className={`size-6 shrink-0 ${tokens.muted}`} />
            <span
              aria-hidden
              className="flex size-12 shrink-0 items-center justify-center rounded-full bg-neutral-700"
            >
              <ArrowUpIcon className="size-6 text-neutral-200" />
            </span>
          </div>
        </div>
      </div>
    </div>
  );
};

/**
 * Claude keeps the prompt box clean and hangs the model rail below it, right
 * aligned. The effort chip is the last thing before the send button and it never
 * changes size — so the card, pinned to that chip's right edge, is the only thing
 * on screen that moves when you open it.
 */
const ClaudeComposer = ({ knobX, theme, onToggle, open, popover }: ComposerChromeProps) => {
  const tokens = EFFORT_THEMES[theme];
  /** Session chips and the model rail share one pill: 24px tall, 6px radius, 6px
   * of padding either side — doubled. */
  const pill = "flex h-12 items-center gap-3 rounded-xl px-3 text-[26px]";

  return (
    <div className="select-none">
      {/* Long enough to survive the crop: the frame swallows the first 180px, so a
          two-chip rail would vanish entirely into the overhang. */}
      <div aria-hidden className={`mb-2.5 flex items-center gap-2.5 ${tokens.text}`}>
        <span className={`${pill} bg-white/8`}>
          <FolderIcon className="size-6" />
          loremllm
        </span>
        <span className={`${pill} bg-white/8`}>
          <GitBranchIcon className="size-6" />
          main
        </span>
        <span className={`${pill} bg-white/8`}>
          <PlusIcon className="size-6" />
        </span>
      </div>

      {/* Same surface and same inset stroke as the popover card — in this app the
          prompt box and the card are the same material, lit the same way. */}
      <div
        aria-hidden
        className="flex items-center gap-4 rounded-[20px] bg-[#2c2c2a] px-5 py-6 shadow-[inset_0_0_0_2px_rgba(255,255,255,0.08)]"
      >
        <p className={`flex-1 text-[28px] ${tokens.muted}`}>Describe a task or ask a question</p>
        <CornerDownLeftIcon className={`size-7 shrink-0 ${tokens.faint}`} />
      </div>

      <div className="mt-2.5 flex h-14 items-center gap-2.5">
        <span aria-hidden className={`flex items-center gap-5 px-3 text-[24px] ${tokens.text}`}>
          Accept edits
          <PlusIcon className="size-6" />
          <MicIcon className="size-6" />
        </span>

        <span aria-hidden className={`ml-auto px-3 text-[24px] ${tokens.text}`}>
          {MODEL_NAME}
        </span>

        <div className="relative" data-effort-control>
          {popover}

          <button
            type="button"
            onClick={onToggle}
            aria-expanded={open}
            // The chip never resizes — open, it just takes on a surface, and the
            // card lands above it.
            className={`flex h-10 cursor-pointer items-center rounded-[10px] px-3 text-[24px] text-white outline-none focus-visible:ring-2 ${
              tokens.ring
            } ${open ? "bg-white/12" : "hover:bg-white/8"}`}
          >
            <LevelLabel knobX={knobX} theme={theme} />
          </button>
        </div>

        <span aria-hidden className="size-10 shrink-0 rounded-full border-[3px] border-white/20" />
      </div>
    </div>
  );
};

/**
 * The composer the picker lives in. Each theme gets its own shell rather than one
 * shell wearing two coats: the two apps don't just paint their controls
 * differently, they *put them in different places* — ChatGPT keeps the effort chip
 * inside the prompt box, Claude parks it on a rail underneath.
 */
export const ComposerChrome = (props: ComposerChromeProps) =>
  props.theme === "claude" ? <ClaudeComposer {...props} /> : <CodexComposer {...props} />;
```

### `/curl-fanfare.tsx`

```tsx
"use client";

import { motion } from "motion/react";

import type { EffortTheme } from "./effort-theme";

import { CARD_PADDING, KNOB_SIZE, TRACK_TRAVEL } from "./effort-scale";
import { EFFORT_THEMES, notchX } from "./effort-theme";

/**
 * The payoff for ten seconds of curling in front of a webcam.
 *
 * Everything here fires from one point — the notch the dial actually landed on —
 * so the celebration is *about the number you earned*, not a generic confetti
 * cannon bolted to the card. Land on Light and the fireworks go off over on the
 * left, next to nothing; land on Ultra and they go off at the end of the track.
 *
 * And it is not the same show at both ends. The whole thing is scaled by
 * `power` — how far up the track you actually got — so Light gets a shrug and
 * Ultra gets the works. Handing out identical fireworks for four curls and for
 * sixteen would undo the only thing the card is measuring.
 *
 * Layers, in the order the eye reads them: the fill ignites and a bloom opens at
 * the notch, an anamorphic flare rips along the track, streaks fan out, rings
 * chase them, and the whole thing rains sparks over the card while the rim
 * lights up. The last three of those don't show up at all until you've earned
 * them.
 */

/** How long the layer stays mounted, in ms. The card tears it down after this —
 * every animation below has finished and faded by then, at any power. */
export const FANFARE_MS = 1700;

/** Ray and spark counts at full power. Both scale down to a handful at Light. */
const STREAK_COUNT = 15;
const SPARK_COUNT = 30;
/** Streaks start this far out from the notch instead of at it. Fifteen bars
 * converging on one pixel screen-blend themselves into a solid white disc — the
 * hollow core is what keeps the burst reading as separate rays. */
const STREAK_GAP = 22;
/**
 * The burst fires from the notch, which sits on the bottom edge of the card — so
 * a tidy 360° fan throws most of itself straight into an edge and is gone. Rays
 * and sparks are aimed across this arc instead: centred straight up, wide enough
 * to reach both top corners. Screen degrees, where -90 is up and 0 is right.
 */
const FAN_FROM = -205;
const FAN_TO = 25;
/** ...and the whole arc leans away from whichever end of the track it landed on,
 * by up to this many degrees. A Light take fires from the left edge and an Ultra
 * one from the right; without the lean, half of either burst is clipped on the frame
 * it's born. */
const FAN_LEAN = 30;

/** Power below which a layer sits the celebration out entirely. The glare rake
 * and the third shockwave are the two loudest things here; a four-curl take has
 * not bought either. */
const GLARE_FLOOR = 0.45;

/** violet-400, as raw channels — half of these colours are built inside template
 * strings for `boxShadow` and gradients, where a Tailwind class can't reach. */
const VIOLET = "167,139,250";

const lerp = (from: number, to: number, t: number) => from + (to - from) * t;

/**
 * How much celebration the take earned, 0 (bottom notch) to 1 (top). Exported
 * because the card scales its own share of the landing — the flashbulb, the
 * knob's slam, the kick, the verdict's halo — off the same number, and a
 * celebration that disagreed with itself about how big it was would read as a
 * bug rather than a scale.
 */
export const fanfarePower = (theme: EffortTheme, level: number) =>
  notchX(theme, level) / TRACK_TRAVEL;

/**
 * Deterministic 0..1 from an index. The spray has to look scattered without
 * being *actually* random: `Math.random()` would reshuffle every particle on
 * every re-render, and the card re-renders plenty while this is on screen.
 */
const scatter = (index: number, salt: number) => {
  const value = Math.sin(index * 12.9898 + salt * 78.233) * 43_758.5453;
  return value - Math.floor(value);
};

interface PoweredProps {
  power: number;
}

/**
 * The soft glow under everything else. Deliberately smaller than the card even
 * at full power: an earlier pass sized it to swallow the whole popover and the
 * result was a white rectangle with a knob in it. The celebration has to stay
 * *local to the notch*, or the card stops being a card for half a second.
 */
const Bloom = ({ power }: PoweredProps) => (
  <motion.span
    className="absolute -translate-x-1/2 -translate-y-1/2 rounded-full"
    style={{
      backgroundImage: `radial-gradient(circle, rgba(${VIOLET},0.55) 0%, rgba(139,92,246,0.3) 30%, rgba(109,40,217,0.14) 52%, transparent 72%)`,
      height: lerp(170, 440, power),
      width: lerp(170, 440, power),
    }}
    initial={{ opacity: 0, scale: 0.12 }}
    animate={{ opacity: [0, lerp(0.55, 1, power), 0], scale: [0.12, 1, 1.25] }}
    transition={{ duration: lerp(0.7, 1.15, power), ease: "easeOut", times: [0, 0.16, 1] }}
  />
);

/** The anamorphic streak along the track axis. One horizontal bar does more for
 * "this is a light source" than any amount of radial spray — which is why it's
 * the one ray that survives all the way down to Light, just short and dim. */
const Flare = ({ power }: PoweredProps) => (
  <motion.span
    className="absolute mix-blend-screen"
    style={{
      backgroundImage: `linear-gradient(90deg, rgba(255,255,255,0) 0%, rgba(${VIOLET},0.7) 30%, rgba(255,255,255,0.95) 50%, rgba(${VIOLET},0.7) 70%, rgba(255,255,255,0) 100%)`,
      borderRadius: 4,
      filter: "blur(1.5px)",
      height: 4,
      width: lerp(160, 480, power),
      x: lerp(-80, -240, power),
      y: -2,
    }}
    initial={{ opacity: 0, scaleX: 0.05, scaleY: 0.4 }}
    animate={{
      opacity: [0, lerp(0.5, 0.9, power), 0],
      scaleX: [0.05, 1, 1.1],
      scaleY: [0.4, 1, 0.3],
    }}
    transition={{ duration: lerp(0.6, 0.95, power), ease: "easeOut", times: [0, 0.14, 1] }}
  />
);

/**
 * A single bar of glare raked across the whole card, once. The notch burst is
 * pinned to one corner by definition; this is the only layer that touches every
 * part of the card, and it's what makes the celebration feel card-sized rather
 * than corner-sized. Which is exactly why a low take doesn't get one — it is the
 * difference between "you finished" and "look at this card".
 */
const Glare = ({ power }: PoweredProps) => (
  <motion.span
    className="absolute -top-1/4 h-[150%] w-28 mix-blend-screen"
    style={{
      backgroundImage: `linear-gradient(90deg, rgba(255,255,255,0) 0%, rgba(221,214,254,0.22) 45%, rgba(255,255,255,0.34) 55%, rgba(255,255,255,0) 100%)`,
      left: -120,
      rotate: 14,
    }}
    initial={{ opacity: 0, x: 0 }}
    animate={{ opacity: [0, power, power, 0], x: [0, 660] }}
    transition={{ duration: 0.7, ease: "easeOut", times: [0, 0.15, 0.7, 1] }}
  />
);

/**
 * The fan of glowing streaks. Each bar hangs off a wrapper that does nothing but
 * rotate about the notch — the split is what lets the bar sit `STREAK_GAP` out
 * along its own axis. Rotating a pre-offset bar directly would swing the offset
 * with it, since motion applies translation before rotation.
 *
 * No `filter: blur()` on these, deliberately. Fifteen filtered layers all
 * appearing on the same frame promote fifteen render surfaces at once, and the
 * first composited frame of the burst can come out as one white flash. The soft
 * edge is baked into the gradient instead, which costs nothing.
 */
const Streaks = ({ power, lean }: PoweredProps & { lean: number }) => {
  // Rays thin out *and* shorten as the take gets weaker — dropping only the
  // count leaves three full-length spears, which reads as a broken burst rather
  // than a small one.
  const count = Math.round(lerp(3, STREAK_COUNT, power));

  return (
    <>
      {Array.from({ length: count }, (_, index) => {
        const angle =
          lean + FAN_FROM + ((index + scatter(index, 1) * 0.7) / count) * (FAN_TO - FAN_FROM);
        const length = lerp(55, 110, power) + scatter(index, 2) * lerp(70, 230, power);
        const thickness = 2 + scatter(index, 3) * 3.5;

        return (
          <span
            key={index}
            className="absolute size-0"
            style={{ rotate: `${angle}deg`, transformOrigin: "0% 0%" }}
          >
            <motion.span
              className="absolute mix-blend-screen"
              style={{
                backgroundImage: `linear-gradient(90deg, rgba(255,255,255,0.95) 0%, rgba(${VIOLET},0.85) 30%, rgba(139,92,246,0.35) 65%, rgba(139,92,246,0) 100%)`,
                borderRadius: thickness,
                height: thickness,
                left: STREAK_GAP,
                top: -thickness / 2,
                transformOrigin: "0% 50%",
                width: length,
              }}
              initial={{ opacity: 0, scaleX: 0 }}
              animate={{ opacity: [0, lerp(0.7, 1, power), 0], scaleX: [0, 1, 1] }}
              transition={{
                delay: scatter(index, 5) * 0.12,
                duration: 0.75 + scatter(index, 4) * 0.5,
                ease: "easeOut",
                times: [0, 0.24, 1],
              }}
            />
          </span>
        );
      })}
    </>
  );
};

/**
 * Shockwaves, staggered — one at the bottom of the track, three at the top.
 * Width and height are animated rather than `scale` so the stroke stays
 * hairline-thin as the ring grows; a scaled border fattens up and stops reading
 * as a shockwave.
 */
const Rings = ({ power }: PoweredProps) => (
  <>
    {[0, 0.1, 0.22].slice(0, 1 + Math.round(power * 2)).map((delay, index) => (
      <motion.span
        key={delay}
        className="absolute -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-violet-200/70 mix-blend-screen"
        style={{ boxShadow: `0 0 22px rgba(${VIOLET},0.55)` }}
        initial={{ height: 20, opacity: 0, width: 20 }}
        animate={{
          height: lerp(130, 280, power) + index * 110,
          opacity: [0, lerp(0.5, 0.8, power), 0],
          width: lerp(130, 280, power) + index * 110,
        }}
        transition={{ delay, duration: 0.95, ease: [0.16, 1, 0.3, 1], times: [0, 0.1, 1] }}
      />
    ))}
  </>
);

/** Ballistic sparks: a fan launched up and out of the notch, then dropped. The
 * two-segment easing is what sells it — `easeOut` on the way up, `easeIn` on the
 * way down, because a firework that decelerates into the floor looks like it's
 * being lowered on a wire. */
const SparkSpray = ({ power, lean }: PoweredProps & { lean: number }) => {
  const count = Math.round(lerp(4, SPARK_COUNT, power));

  return (
    <>
      {Array.from({ length: count }, (_, index) => {
        const heading =
          ((lean + FAN_FROM + scatter(index, 6) * (FAN_TO - FAN_FROM)) * Math.PI) / 180;
        const speed = lerp(50, 90, power) + scatter(index, 7) * lerp(90, 320, power);
        const dx = Math.cos(heading) * speed;
        const dy = Math.sin(heading) * speed;
        const size = 3 + scatter(index, 8) * 4;
        const elongated = index % 3 === 0;

        return (
          <motion.span
            key={index}
            className="absolute"
            style={{
              background: index % 2 ? `rgb(${VIOLET})` : "#ffffff",
              borderRadius: size,
              boxShadow: `0 0 ${6 + size}px rgba(${VIOLET},0.85)`,
              height: size,
              // Centred with margins, not a translate class: the keyframes below
              // own the transform outright and would overwrite one.
              marginLeft: -size / 2,
              marginTop: -size / 2,
              rotate: (heading * 180) / Math.PI,
              width: elongated ? size * 3.5 : size,
            }}
            initial={{ opacity: 1, scale: 1, x: 0, y: 0 }}
            animate={{
              opacity: [1, 1, 0],
              scale: [1, 1, 0.35],
              x: [0, dx, dx * 1.25],
              y: [0, dy, dy + 210],
            }}
            transition={{
              duration: 1.05 + scatter(index, 9) * 0.45,
              ease: ["easeOut", "easeIn"],
              times: [0, 0.45, 1],
            }}
          />
        );
      })}
    </>
  );
};

interface CurlFanfareProps {
  /** The level the take rounded down to — the show's origin point, and its size. */
  level: number;
  theme: EffortTheme;
}

export const CurlFanfare = ({ level, theme }: CurlFanfareProps) => {
  const tokens = EFFORT_THEMES[theme];
  const landedX = notchX(theme, level);
  const power = fanfarePower(theme, level);
  // Card coordinates, not track coordinates: the fanfare covers the whole card,
  // so the notch has to be re-expressed against the card's own padding box. The
  // track sits flush in that box, which makes this the knob's centre.
  const anchorLeft = CARD_PADDING + KNOB_SIZE / 2 + landedX;
  const anchorBottom = CARD_PADDING + KNOB_SIZE / 2;
  const lean = (0.5 - power) * 2 * FAN_LEAN;

  return (
    <>
      {/* The card's own edge, lit. Unclipped, so the outer bloom escapes the
          popover the way a real light source would — faintly at Light, where it
          reads as the card catching a little of the glow rather than announcing
          anything. */}
      <motion.span
        aria-hidden
        className="pointer-events-none absolute inset-0 rounded-[inherit]"
        style={{
          boxShadow: `inset 0 0 0 2px rgba(196,181,253,0.7), inset 0 0 40px rgba(${VIOLET},0.28), 0 0 60px rgba(139,92,246,0.45)`,
        }}
        initial={{ opacity: 0 }}
        animate={{ opacity: [0, lerp(0.22, 1, power), 0] }}
        transition={{ duration: 1.3, ease: "easeOut", times: [0, 0.1, 1] }}
      />

      {/* Everything with a trajectory lives in here, clipped to the card. Sparks
          that outlive the card's edge should die at it — a firework that leaks
          out of the popover reads as a rendering bug, not a flourish.

          `isolate` matters as much as the clip: every layer below blends screen,
          and without an isolation group they blend against the whole card —
          including the verdict's `backdrop-blur` panel, which Chrome composites
          in a different pass. Left un-isolated they stack into one flat white
          disc over the camera. Isolated, they screen against each other and the
          group lands on the card as ordinary alpha. */}
      <span
        aria-hidden
        className="pointer-events-none absolute inset-0 overflow-hidden rounded-[inherit] isolate"
      >
        {/* The fill charges: a hot band runs the length of everything the take
            earned, arriving at the notch just as the knob commits to it. This one
            layer needs no scaling — it *is* the scale, since there's barely any
            fill to run down at the bottom of the track. */}
        <span
          className="absolute overflow-hidden"
          style={{
            borderRadius: tokens.troughRadius,
            bottom: CARD_PADDING + (KNOB_SIZE - tokens.troughHeight) / 2,
            height: tokens.troughHeight,
            left: CARD_PADDING,
            width: landedX + KNOB_SIZE / 2,
          }}
        >
          <motion.span
            className="absolute inset-y-0 w-1/2 mix-blend-screen"
            style={{
              backgroundImage:
                "linear-gradient(90deg, rgba(255,255,255,0) 0%, rgba(255,255,255,0.85) 55%, rgba(255,255,255,0) 100%)",
            }}
            initial={{ x: "-130%" }}
            animate={{ x: "320%" }}
            transition={{ duration: 0.65, ease: "easeOut" }}
          />
        </span>

        {power >= GLARE_FLOOR && <Glare power={power} />}

        {/* Zero-sized anchor at the notch. Everything below centres on it the
            same way the per-rep `Sparks` do — position once, then only ever
            animate transforms. */}
        <span className="absolute size-0" style={{ bottom: anchorBottom, left: anchorLeft }}>
          <Bloom power={power} />
          <Flare power={power} />
          <Streaks power={power} lean={lean} />
          <Rings power={power} />
          <SparkSpray power={power} lean={lean} />
        </span>
      </span>
    </>
  );
};
```

### `/curl-track.tsx`

```tsx
"use client";

import { useCallback, useEffect, useRef, useState } from "react";
import { animate, AnimatePresence, motion, useMotionValue, useReducedMotion } from "motion/react";
import { DumbbellIcon, RotateCcwIcon, VideoOffIcon } from "lucide-react";

import type { MotionValue } from "motion/react";

import type { EffortTheme } from "./effort-theme";

import { CurlFanfare, fanfarePower, FANFARE_MS } from "./curl-fanfare";
import { DialTrough, knobBoxStyle, KnobSkin, Sparks } from "./effort-dial";
import { clamp, KNOB_SIZE, percentToX, TRACK_WIDTH } from "./effort-scale";
import { EFFORT_THEMES, labelAt, nearestLevel, notchX } from "./effort-theme";
import { usePoseCurls } from "./use-pose-curls";

import type { PoseStatus } from "./use-pose-curls";

/** The whole take, in milliseconds. Long enough to hurt, short enough to redo. */
const SET_MS = 10_000;
const COUNT_MS = 900;
/** Effort a completed curl buys you, in percent. One arm is watched, so this is
 * the whole of your income. Just over a full notch, so every rep visibly moves
 * the verdict — and the ladder it sets against the drain below: five curls in
 * the set is Medium-ish, eight is Ultra. */
const CURL_GAIN = 27;
/** Effort the machine takes back every second, whatever you're doing. Against the
 * gain above, holding a level costs a curl every 2.7 seconds — anything less and
 * the dial walks backwards while you watch it. */
const DRAIN_PER_SEC = 10;

const COMMIT_SPRING = { damping: 18, stiffness: 180, type: "spring" } as const;
const KNOB_PUMP = { damping: 18, stiffness: 700, type: "spring" } as const;
/** Looser than the per-rep pump and thrown from much further out: the landing
 * kick is the one the whole take has been building to. */
const KNOB_SLAM = { damping: 11, stiffness: 420, type: "spring" } as const;

const lerp = (from: number, to: number, t: number) => from + (to - from) * t;
/** Power below which the verdict keeps its halo and drops the rest of its
 * ceremony. Four curls is a result, not an occasion. */
const SUNBURST_FLOOR = 0.45;

type CountdownCount = 3 | 2 | 1;
interface Burst {
  id: number;
  percent: number;
}

type TakeState =
  | { status: "arming" }
  | { status: "countdown"; count: CountdownCount }
  | { status: "lifting" }
  | { status: "complete"; level: number };

const COUNTDOWN_PROMPTS: Record<CountdownCount, string> = {
  1: "Curl!",
  2: "Arm down",
  3: "Get in frame",
};

const Overlay = ({ children }: { children: React.ReactNode }) => (
  <motion.div
    initial={{ opacity: 0 }}
    animate={{ opacity: 1 }}
    exit={{ opacity: 0 }}
    transition={{ duration: 0.18 }}
    className="absolute inset-0 flex flex-col items-center justify-center gap-2 bg-neutral-950/45 px-4 backdrop-blur-[2px]"
  >
    {children}
  </motion.div>
);

interface VerdictProps {
  label: string;
  power: number;
  reduceMotion: boolean;
  reps: number;
}

const Verdict = ({ label, power, reduceMotion, reps }: VerdictProps) => (
  <motion.div
    initial={reduceMotion ? { opacity: 0 } : { opacity: 0, scale: 0.9 }}
    animate={{ opacity: 1, scale: 1 }}
    transition={reduceMotion ? { duration: 0.1 } : { damping: 20, stiffness: 320, type: "spring" }}
    className="relative flex flex-col items-center gap-1.5"
  >
    {!reduceMotion && (
      <>
        {/* A halo that blows open and then stays, dimmed — the
            verdict keeps a light on it for as long as it's up. */}
        <motion.span
          aria-hidden
          className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded-full mix-blend-screen"
          style={{
            backgroundImage:
              "radial-gradient(circle, rgba(167,139,250,0.75) 0%, rgba(139,92,246,0.4) 34%, rgba(109,40,217,0.16) 56%, transparent 72%)",
            height: lerp(140, 256, power),
            width: lerp(140, 256, power),
          }}
          initial={{ opacity: 0, scale: 0.25 }}
          animate={{
            opacity: [0, lerp(0.45, 1, power), lerp(0.3, 0.65, power)],
            scale: [0.25, 1.15, 1],
          }}
          transition={{ duration: 0.85, ease: "easeOut", times: [0, 0.35, 1] }}
        />
        {/* A sunburst, turning slowly. It's a repeating conic
            gradient masked back to a disc — twelve wedges of light
            behind the word, which is the cheapest way to make a
            static label look like it's radiating. Reserved for the
            top half of the track: a Light take gets the halo and
            nothing to stand in front of. */}
        {power >= SUNBURST_FLOOR && (
          <motion.span
            aria-hidden
            className="absolute top-1/2 left-1/2 size-72 -translate-x-1/2 -translate-y-1/2 rounded-full mix-blend-screen"
            style={{
              backgroundImage:
                "repeating-conic-gradient(rgba(196,181,253,0.34) 0deg 8deg, transparent 8deg 30deg)",
              maskImage: "radial-gradient(circle, #000 0%, rgba(0,0,0,0.55) 42%, transparent 70%)",
            }}
            initial={{ opacity: 0, rotate: -14, scale: 0.4 }}
            animate={{ opacity: [0, power, power * 0.4], rotate: 12, scale: 1 }}
            transition={{ duration: 2.4, ease: "easeOut", times: [0, 0.14, 1] }}
          />
        )}
        {[0, 0.13].slice(0, power >= SUNBURST_FLOOR ? 2 : 1).map((delay) => (
          <motion.span
            key={delay}
            aria-hidden
            className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded-full border border-violet-200/70 shadow-[0_0_24px_rgba(167,139,250,0.6)]"
            initial={{ height: 40, opacity: 0, width: 40 }}
            animate={{
              height: lerp(150, 300, power),
              opacity: [0, lerp(0.5, 0.85, power), 0],
              width: lerp(150, 300, power),
            }}
            transition={{
              delay,
              duration: 1,
              ease: [0.16, 1, 0.3, 1],
              times: [0, 0.12, 1],
            }}
          />
        ))}
      </>
    )}
    <p className="relative flex flex-col items-center gap-0.5">
      <span className="text-[13px] tracking-wide text-neutral-400 uppercase">Landed on</span>
      {/* The word itself gets the pop, a beat behind the card's:
          the fireworks announce it, then it arrives. */}
      <motion.span
        initial={reduceMotion ? false : { opacity: 0, scale: 0.6 }}
        animate={{ opacity: 1, scale: 1 }}
        transition={{
          damping: 13,
          delay: reduceMotion ? 0 : 0.08,
          stiffness: 520,
          type: "spring",
        }}
        // White, not violet: the sunburst behind it is violet and
        // bright, and the word has to stay the lightest thing on
        // the card or the ceremony swallows the verdict. It grows
        // with the take as well — Ultra should be a headline and
        // Light shouldn't be.
        style={{
          filter: `drop-shadow(0 0 ${lerp(12, 26, power)}px rgba(139,92,246,0.95))`,
          fontSize: lerp(20, 30, power),
        }}
        className="leading-none font-semibold text-white"
      >
        {label}
      </motion.span>
    </p>
    <p className="relative text-[13px] tabular-nums text-neutral-400">
      {reps} {reps === 1 ? "curl" : "curls"} in 10 seconds
    </p>
  </motion.div>
);

interface CameraOverlaysProps {
  error: string | null;
  failed: boolean;
  power: number;
  reduceMotion: boolean;
  reps: number;
  status: PoseStatus;
  take: TakeState;
  theme: EffortTheme;
}

const CameraOverlays = ({
  error,
  failed,
  power,
  reduceMotion,
  reps,
  status,
  take,
  theme,
}: CameraOverlaysProps) => (
  <AnimatePresence mode="wait" initial={false}>
    {failed && (
      <Overlay key="error">
        <VideoOffIcon className="size-6 text-rose-400" />
        <p className="max-w-[280px] text-center text-[13px] text-neutral-300">
          {error ?? "The camera said no."} Effort cannot be verified.
        </p>
      </Overlay>
    )}
    {!failed && take.status === "arming" && (
      <Overlay key="arming">
        <p className="text-[15px] font-medium text-neutral-100">
          {status === "tracking" ? "Show me an arm" : "Waking the camera…"}
        </p>
      </Overlay>
    )}
    {!failed && take.status === "countdown" && (
      <Overlay key="countdown">
        <span className="text-[44px] leading-none font-semibold tabular-nums text-violet-300 drop-shadow-[0_2px_12px_rgba(139,92,246,0.55)]">
          {take.count}
        </span>
        <p className="text-[15px] font-medium text-neutral-100">{COUNTDOWN_PROMPTS[take.count]}</p>
      </Overlay>
    )}
    {!failed && take.status === "complete" && (
      <Overlay key="landed">
        <Verdict
          label={labelAt(theme, take.level)}
          power={power}
          reduceMotion={reduceMotion}
          reps={reps}
        />
      </Overlay>
    )}
  </AnimatePresence>
);

interface CurlCardProps {
  knobX: MotionValue<number>;
  theme: EffortTheme;
}

/**
 * The effort picker as a physical exam. There is no knob to drag — there's a
 * camera, a ten second clock, and your arms.
 *
 * MediaPipe watches one elbow — whichever it can see best. Every curl pumps the
 * dial a little way up the track; the track leaks the whole time, so the needle
 * is only ever as high as your last few seconds of work. Whatever's left when the
 * clock runs out gets rounded to the nearest notch, and that's the effort the
 * model gets. Stop curling for two seconds and you have talked yourself down to
 * Light.
 */
export const CurlCard = ({ knobX, theme }: CurlCardProps) => {
  const [take, setTake] = useState<TakeState>({ status: "arming" });
  const [reps, setReps] = useState(0);
  const [bursts, setBursts] = useState<Burst[]>([]);
  /** The landing fireworks, mounted for exactly as long as they burn. */
  const [celebrating, setCelebrating] = useState(false);
  const reduceMotion = Boolean(useReducedMotion());

  const cardPop = useMotionValue(1);
  const knobScale = useMotionValue(1);
  const timerScale = useMotionValue(1);
  const timerRef = useRef<HTMLSpanElement>(null);
  const effortRef = useRef(0);
  const burstId = useRef(0);
  /** The rep handler is called from the detection loop, which knows nothing about
   * React's render cycle — it reads the phase through a ref. */
  const phaseRef = useRef<TakeState["status"]>("arming");
  const reduceMotionRef = useRef(reduceMotion);

  useEffect(() => {
    phaseRef.current = take.status;
  }, [take.status]);
  useEffect(() => {
    reduceMotionRef.current = reduceMotion;
  }, [reduceMotion]);

  const handleRep = useCallback(() => {
    if (phaseRef.current !== "lifting") {
      return;
    }
    effortRef.current = clamp(effortRef.current + CURL_GAIN, 0, 100);
    knobX.set(percentToX(effortRef.current));
    setReps((count) => count + 1);

    if (reduceMotionRef.current) {
      return;
    }
    // The knob takes the hit: a kick on every rep, settling back while the drain
    // quietly walks it left again.
    knobScale.set(1.18);
    void animate(knobScale, 1, KNOB_PUMP);
    burstId.current += 1;
    const burst = { id: burstId.current, percent: effortRef.current };
    setBursts((current) => [...current, burst]);
    window.setTimeout(
      () => setBursts((current) => current.filter((item) => item.id !== burst.id)),
      700,
    );
  }, [knobX, knobScale]);

  const { videoRef, canvasRef, status, error, posed, retry } = usePoseCurls({
    enabled: true,
    onRep: handleRep,
  });

  const failed = status === "error";

  const run = useCallback(() => {
    effortRef.current = 0;
    setReps(0);
    setBursts([]);
    setCelebrating(false);
    setTake({ status: "arming" });
    timerScale.set(1);
    cardPop.set(1);
    knobScale.set(1);
    if (reduceMotionRef.current) {
      knobX.set(0);
    } else {
      void animate(knobX, 0, COMMIT_SPRING);
    }
  }, [knobX, timerScale, cardPop, knobScale]);

  // The fireworks tear themselves down. Nothing in `CurlFanfare` loops, so the
  // only reason to keep three dozen animated spans mounted past their last
  // frame would be forgetting to unmount them.
  useEffect(() => {
    if (!celebrating) {
      return;
    }
    const timer = window.setTimeout(() => setCelebrating(false), FANFARE_MS);
    return () => window.clearTimeout(timer);
  }, [celebrating]);

  // Start the count-in only after the camera detects an arm.
  if (take.status === "arming" && status === "tracking" && posed) {
    setTake({ count: 3, status: "countdown" });
  }

  useEffect(() => {
    if (take.status !== "countdown") {
      return;
    }
    const { count } = take;
    const timer = window.setTimeout(() => {
      if (count === 1) {
        setTake({ status: "lifting" });
      } else {
        setTake({ count: count === 3 ? 2 : 1, status: "countdown" });
      }
    }, COUNT_MS);
    return () => window.clearTimeout(timer);
  }, [take]);

  // The set itself: one loop, draining effort and the clock together. Both are
  // written straight to the DOM / motion values — a ten second countdown that
  // re-renders the card sixty times a second would be a strange way to spend a
  // frame budget the pose model is already using.
  useEffect(() => {
    if (take.status !== "lifting") {
      return;
    }

    let frame = 0;
    const startedAt = performance.now();
    let previous = startedAt;

    const tick = (now: number) => {
      const elapsed = now - startedAt;
      const delta = (now - previous) / 1000;
      previous = now;

      effortRef.current = clamp(effortRef.current - DRAIN_PER_SEC * delta, 0, 100);
      knobX.set(percentToX(effortRef.current));

      const remaining = Math.max(0, SET_MS - elapsed);
      timerScale.set(remaining / SET_MS);
      if (timerRef.current) {
        timerRef.current.textContent = (remaining / 1000).toFixed(1);
      }

      if (remaining > 0) {
        frame = requestAnimationFrame(tick);
        return;
      }

      // Time's up. The dial has notches, not percentages, so the last thing the
      // set does is round away most of what you just earned.
      const level = nearestLevel(theme, percentToX(effortRef.current));
      setTake((current) =>
        current.status === "lifting" ? { level, status: "complete" } : current,
      );
      if (reduceMotionRef.current) {
        knobX.set(notchX(theme, level));
      } else {
        // The drain writes knobX every frame, so the value arrives at the commit
        // carrying real velocity — a spring that inherits it hurls the knob clean
        // off the end of the track. The take is over; it starts from rest.
        void animate(knobX, notchX(theme, level), { ...COMMIT_SPRING, velocity: 0 });
        // The knob doesn't just arrive at the notch, it lands on it: a slam the
        // card feels, with the fireworks going off from the point of impact.
        // `velocity: 0` for the same reason the commit above needs it, and then
        // some: a `set()` one tick before an `animate()` reads as a jump of 0.55
        // in a single frame, so the spring inherits a velocity of ~33/s. Left to
        // pick that up, this one underdamped spring blew the knob to 36× its own
        // size — a white disc wider than the card, for about a fifth of a second.
        //
        // How hard it lands is the take's own doing: at the bottom of the track
        // this is a nudge and the card barely moves, at the top it's a slam.
        const power = fanfarePower(theme, level);
        knobScale.set(lerp(1.16, 1.55, power));
        void animate(knobScale, 1, { ...KNOB_SLAM, velocity: 0 });
        void animate(cardPop, [1, lerp(1.008, 1.035, power), 0.997, 1], {
          duration: 0.6,
          ease: "easeOut",
        });
        setCelebrating(true);
      }
    };

    frame = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(frame);
  }, [take.status, knobX, knobScale, cardPop, timerScale, theme]);

  // How much of the ceremony the take earned, 0 at the bottom notch and 1 at the
  // top. Everything the card itself does for the landing is scaled by it, the
  // same way `CurlFanfare` scales the fireworks.
  const power = take.status === "complete" ? fanfarePower(theme, take.level) : 0;
  const live = take.status === "lifting";

  return (
    <motion.div style={{ scale: cardPop }} className={`relative p-5 ${EFFORT_THEMES[theme].card}`}>
      <div className="mb-4 flex h-8 items-center justify-between gap-3">
        <p className="text-[15px] font-medium text-neutral-100">Reasoning effort</p>

        <AnimatePresence mode="wait" initial={false}>
          {(take.status === "complete" || failed) && (
            <motion.button
              key="again"
              type="button"
              onClick={failed ? retry : run}
              initial={{ opacity: 0, scale: 0.9 }}
              animate={{ opacity: 1, scale: 1 }}
              exit={{ opacity: 0, scale: 0.9 }}
              transition={{ duration: 0.16 }}
              className="flex shrink-0 items-center gap-1.5 rounded-full border border-violet-500/60 bg-violet-500/10 px-3 py-1.5 text-[13px] text-neutral-100 outline-none focus-visible:ring-2 focus-visible:ring-violet-500/60"
            >
              <RotateCcwIcon className="size-3.5 text-violet-300" />
              {failed ? "Retry camera" : "Again"}
            </motion.button>
          )}
          {live && !failed && (
            <motion.span
              key="reps"
              initial={{ opacity: 0 }}
              animate={{ opacity: 1 }}
              exit={{ opacity: 0 }}
              className="flex shrink-0 items-center gap-1.5 text-[13px] tabular-nums text-neutral-400"
            >
              <DumbbellIcon className="size-3.5 text-violet-300" />
              {reps} {reps === 1 ? "rep" : "reps"}
            </motion.span>
          )}
        </AnimatePresence>
      </div>

      {/* The camera is the card's face, and it's square: a letterboxed strip of a
          webcam is a status light, but a full frame of you is a mirror. It stays on
          through the verdict — you should have to watch yourself be told you
          managed Medium. */}
      <div className="relative mb-4 aspect-square overflow-hidden rounded-2xl bg-neutral-900/70">
        <video
          ref={videoRef}
          muted
          playsInline
          aria-hidden
          className="size-full -scale-x-100 object-cover"
        />
        <canvas
          ref={canvasRef}
          aria-hidden
          className="absolute inset-0 size-full -scale-x-100 object-cover"
        />
        <div
          aria-hidden
          className="absolute inset-0 bg-gradient-to-t from-neutral-950/80 via-transparent to-neutral-950/40"
        />

        <div aria-live="polite" className="absolute inset-0">
          <CameraOverlays
            error={error}
            failed={failed}
            power={power}
            reduceMotion={reduceMotion}
            reps={reps}
            status={status}
            take={take}
            theme={theme}
          />
        </div>

        {/* The clock: a bar that empties, and the number it's emptying towards. */}
        <AnimatePresence>
          {live && (
            <motion.div
              key="clock"
              initial={{ opacity: 0 }}
              animate={{ opacity: 1 }}
              exit={{ opacity: 0 }}
              className="absolute inset-x-3 bottom-3 flex items-center gap-2.5"
            >
              <span className="h-1.5 flex-1 overflow-hidden rounded-full bg-white/15">
                <motion.span
                  className="block h-full origin-left rounded-full bg-violet-400"
                  style={{ scaleX: timerScale }}
                />
              </span>
              <span className="text-[13px] font-medium tabular-nums text-neutral-100">
                <span ref={timerRef}>10.0</span>s
              </span>
            </motion.div>
          )}
        </AnimatePresence>

        {/* Flashbulb. The camera has been pointed at you for ten seconds; when
            the clock stops, it takes the photo. */}
        {celebrating && (
          <motion.span
            aria-hidden
            className="pointer-events-none absolute inset-0 bg-violet-50 mix-blend-screen"
            initial={{ opacity: 0 }}
            animate={{ opacity: [0, lerp(0.1, 0.42, power), 0] }}
            transition={{ duration: 0.55, ease: "easeOut", times: [0, 0.07, 1] }}
          />
        )}
      </div>

      <div className="relative" style={{ height: KNOB_SIZE, width: TRACK_WIDTH }}>
        <DialTrough knobX={knobX} theme={theme} />

        <AnimatePresence>
          {!reduceMotion &&
            bursts.map((burst) => <Sparks key={burst.id} percent={burst.percent} />)}
        </AnimatePresence>

        {/* The knob kicks on every rep — the only thing on the card that answers
            your body faster than the drain can undo it. */}
        <motion.div
          aria-hidden
          className="absolute"
          style={{ ...knobBoxStyle(theme), scale: knobScale, x: knobX }}
        >
          <KnobSkin theme={theme} />
        </motion.div>
      </div>

      {/* Last child, and deliberately: the fanfare is lit from in front of the
          card, so it goes over the camera, the verdict and the dial alike. Every
          layer of it blends screen, which brightens what's underneath instead of
          hiding it — the word you earned stays legible through the fireworks. */}
      {celebrating && take.status === "complete" && (
        <CurlFanfare level={take.level} theme={theme} />
      )}
    </motion.div>
  );
};
```

### `/effort-dial.tsx`

```tsx
"use client";

import { useRef } from "react";
import { motion, useMotionValueEvent, useTransform } from "motion/react";

import type { MotionValue } from "motion/react";

import type { EffortTheme } from "./effort-theme";

import { KNOB_SIZE, percentToX, TRACK_TRAVEL, TRACK_WIDTH } from "./effort-scale";
import { EFFORT_THEMES, labelAt, nearestLevel, notchX } from "./effort-theme";

interface DialTroughProps {
  knobX: MotionValue<number>;
  theme: EffortTheme;
}

/**
 * Everything about the dial that is purely a function of the knob's position:
 * the trough, the fill, the notch dots. All three variants render this; each
 * brings its own knob, because a knob you sling and a knob you sing at behave
 * nothing alike even though they sit in the same groove.
 */
export const DialTrough = ({ knobX, theme }: DialTroughProps) => {
  const tokens = EFFORT_THEMES[theme];
  const offsetY = (KNOB_SIZE - tokens.troughHeight) / 2;

  // The trough follows the knob out past either cap, so the knob always sits ON
  // the track rather than floating off the end of it.
  const troughLeft = useTransform(knobX, (x) => Math.min(0, x));
  const troughWidth = useTransform(
    knobX,
    (x) => TRACK_WIDTH + Math.max(0, -x) + Math.max(0, x - TRACK_TRAVEL),
  );

  const fillLeft = useTransform(knobX, (x) => Math.min(0, x));
  const fillWidth = useTransform(knobX, (x) => x - Math.min(0, x) + KNOB_SIZE / 2);
  const [rampFrom, rampTo] = tokens.fill;
  const fillColor = useTransform(
    knobX,
    [0, TRACK_TRAVEL * 0.75, TRACK_TRAVEL],
    [rampFrom, rampFrom, rampTo],
  );

  return (
    <>
      <motion.div
        className={`absolute ${tokens.trough}`}
        style={{
          borderRadius: tokens.troughRadius,
          height: tokens.troughHeight,
          left: troughLeft,
          top: offsetY,
          width: troughWidth,
        }}
      />

      <motion.div
        className="absolute"
        style={{
          backgroundColor: fillColor,
          borderRadius: tokens.troughRadius,
          height: tokens.troughHeight,
          left: fillLeft,
          top: offsetY,
          width: fillWidth,
        }}
      />

      {tokens.levels.map((effortLabel, index) => (
        <span
          key={effortLabel}
          aria-hidden
          className={`absolute size-1.5 -translate-x-1/2 -translate-y-1/2 rounded-full ${
            index === tokens.levels.length - 1 ? tokens.dotTop : tokens.dot
          }`}
          style={{ left: KNOB_SIZE / 2 + notchX(theme, index), top: "50%" }}
        />
      ))}
    </>
  );
};

/**
 * The visible knob, riding inside the invariant hit box. Sizing it here rather
 * than on the box is what lets Claude's knob sit down inside its groove while
 * Codex's caps the track, without either app's paint reaching the physics.
 */
export const KnobSkin = ({ theme }: { theme: EffortTheme }) => {
  const { knob } = EFFORT_THEMES[theme];

  return (
    <span
      aria-hidden
      className={`absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 ${knob.className}`}
      style={{ borderRadius: knob.radius, height: knob.height, width: knob.width }}
    />
  );
};

/** The hit box every knob is dragged by. Themes paint inside it; nothing about
 * it changes, because `TRACK_TRAVEL` is measured against it. */
export const knobBoxStyle = (theme: EffortTheme) => ({
  borderRadius: EFFORT_THEMES[theme].knob.radius,
  height: KNOB_SIZE,
  left: 0,
  top: 0,
  width: KNOB_SIZE,
});

interface LevelLabelProps {
  knobX: MotionValue<number>;
  theme: EffortTheme;
  className?: string;
}

/**
 * The dial's word for where it is. The knob moves every frame but the *level*
 * only changes when it crosses a notch, so the text is written straight to the
 * DOM rather than re-rendering React — and the same span serves the composer
 * chip and the card header.
 */
export const LevelLabel = ({ knobX, theme, className }: LevelLabelProps) => {
  const ref = useRef<HTMLSpanElement>(null);
  useMotionValueEvent(knobX, "change", (x) => {
    if (ref.current) {
      ref.current.textContent = labelAt(theme, nearestLevel(theme, x));
    }
  });

  // Reading the knob during render is safe: it only ever seeds the first paint,
  // and a card that mounts mid-flight should open on the level it can already see.
  return (
    <span ref={ref} className={className}>
      {labelAt(theme, nearestLevel(theme, knobX.get()))}
    </span>
  );
};

const SPARK_ANGLES = [-140, -110, -75, -45, 45, 75, 110, 140];

/** A hit of confetti at the point on the track a percentage just claimed. Both
 * performed variants throw these — a sung percentage and a completed curl are
 * the same event as far as the dial is concerned. */
export const Sparks = ({ percent }: { percent: number }) => (
  <motion.span
    aria-hidden
    className="absolute top-1/2 size-0"
    style={{ left: percentToX(percent) + KNOB_SIZE / 2 }}
  >
    <motion.span
      className="absolute -translate-x-1/2 -translate-y-1/2 rounded-full border-2 border-violet-400"
      initial={{ height: 10, opacity: 0.9, width: 10 }}
      animate={{ height: 64, opacity: 0, width: 64 }}
      transition={{ duration: 0.55, ease: "easeOut" }}
    />
    {SPARK_ANGLES.map((angle) => (
      <motion.span
        key={angle}
        className="absolute size-1 rounded-full bg-violet-300"
        initial={{ opacity: 1, scale: 1, x: 0, y: 0 }}
        animate={{
          opacity: 0,
          scale: 0.4,
          x: Math.cos((angle * Math.PI) / 180) * 30,
          y: Math.sin((angle * Math.PI) / 180) * 26,
        }}
        transition={{ duration: 0.6, ease: "easeOut" }}
      />
    ))}
  </motion.span>
);
```

### `/effort-picker.tsx`

```tsx
"use client";

import { useState } from "react";
import { AnimatePresence, motion, useMotionValue } from "motion/react";

import type { EffortTheme } from "./effort-theme";

import { ComposerChrome } from "./composer-chrome";
import { CurlCard } from "./curl-track";
import { CARD_WIDTH, DEFAULT_LEVEL } from "./effort-scale";
import { EFFORT_THEMES, nearestLevel, notchX } from "./effort-theme";
import { KaraokeCard } from "./karaoke-track";
import { SlingshotCard } from "./slingshot-track";

export { type EffortTheme } from "./effort-theme";

export type EffortVariant = "slingshot" | "karaoke" | "curls";

const CARDS = { curls: CurlCard, karaoke: KaraokeCard, slingshot: SlingshotCard };

interface EffortPickerProps {
  variant?: EffortVariant;
  /** Which app the picker is pretending to live inside. Orthogonal to variant:
   * the theme paints the chrome, the variant supplies the physics. */
  theme?: EffortTheme;
}

const CARD_POP = { damping: 30, stiffness: 380, type: "spring" } as const;

/** Slingshot is a control you operate. The other two are takes you perform: they
 * start the moment the card opens and run on their own clock. */
const isPerformance = (variant: EffortVariant) => variant !== "slingshot";

/**
 * The Codex effort picker, rebuilt twice as a machine that resents being aimed —
 * and reskinned as either of the two apps that ship one.
 *
 * The whole thing is staged as a cropped corner of an app: the composer and the
 * surfaces behind it run wider than the frame and bleed off the edges, so the
 * popover reads as floating over a real product rather than posing on a page.
 * The shell owns the knob's position so the composer chip can follow it frame by
 * frame without re-rendering React.
 */
export const EffortPicker = ({ variant = "slingshot", theme = "chatgpt" }: EffortPickerProps) => {
  const knobX = useMotionValue(notchX(theme, DEFAULT_LEVEL));
  // The slingshot's popover is up by default — a closed card is a dull first frame.
  // A performance has to be summoned: it needs a curtain, and the curls variant
  // needs the click before it asks anyone for their camera.
  const [open, setOpen] = useState(!isPerformance(variant));

  // Swapping variants on a mounted picker has to re-close the curtain, or karaoke
  // inherits the slingshot's always-open card and starts singing to nobody.
  const [renderedVariant, setRenderedVariant] = useState(variant);
  if (renderedVariant !== variant) {
    setRenderedVariant(variant);
    setOpen(!isPerformance(variant));
  }

  // A theme is free to cut its own number of notches into the track, so a knob
  // parked on one theme's notch need not sit on any of the next one's. Carry the
  // *level* across rather than the position — the dial should read the same word
  // after a reskin as it did before one.
  const [renderedTheme, setRenderedTheme] = useState(theme);
  if (renderedTheme !== theme) {
    const level = nearestLevel(renderedTheme, knobX.get());
    setRenderedTheme(theme);
    knobX.set(notchX(theme, level));
  }

  const handleToggle = () => setOpen((was) => !was);
  const Card = CARDS[variant];

  // Clicking anywhere that isn't the chip or the card dismisses the popover, the
  // way every real popover dies. The chip and card live inside the composer's own
  // markup, so the frame finds them by attribute rather than holding refs into a
  // subtree it doesn't render.
  const handleFramePointerDown = (event: React.PointerEvent) => {
    if (!open) {
      return;
    }
    if (event.target instanceof Element && event.target.closest("[data-effort-control]")) {
      return;
    }
    setOpen(false);
  };

  const popover = (
    <AnimatePresence>
      {open && (
        <motion.div
          // Re-keyed on theme as well as variant: a reskinned card is a different
          // card, and it should land rather than cross-fade its own chrome.
          key={`${variant}-${theme}`}
          initial={{ opacity: 0, scale: 0.96, y: 6 }}
          animate={{ opacity: 1, scale: 1, y: 0 }}
          exit={{ opacity: 0, scale: 0.96, y: 4 }}
          transition={CARD_POP}
          style={{ transformOrigin: "bottom right", width: CARD_WIDTH }}
          // Pinned to the chip's right edge, at its full open width from the first
          // frame: the card doesn't grow with the chip, it lands over where the chip
          // is heading. The chip catches up underneath it.
          className="absolute right-0 bottom-full mb-2.5"
        >
          <Card knobX={knobX} theme={theme} />
        </motion.div>
      )}
    </AnimatePresence>
  );

  return (
    <div
      onPointerDown={handleFramePointerDown}
      className={`relative flex h-full w-full items-end justify-end overflow-hidden ${EFFORT_THEMES[theme].frame}`}
    >
      {/* We're zoomed into the corner of somebody else's app, where the controls live:
          the composer overhangs the left edge and the frame crops it. The zoom comes
          from intrinsic sizes, never a transform — scaling this subtree would leave the
          slingshot's pointer deltas (screen px) out of step with its physics (local px),
          and the knob would outrun your finger.

          The composer sits ON the bottom edge rather than centred, which is where a
          composer actually lives — and it's what buys the curls variant the headroom
          for a square camera without shoving its card off the top of the frame. */}
      <div className="w-[calc(100%+180px)] shrink-0 pr-6 pb-8">
        <ComposerChrome
          knobX={knobX}
          theme={theme}
          open={open}
          onToggle={handleToggle}
          popover={popover}
        />
      </div>
    </div>
  );
};
```

### `/effort-scale.ts`

```ts
/**
 * Geometry shared by the picker shell and its tracks. The physics is written in
 * track-pixels, so the track has a fixed width — every constant here is derived
 * from it rather than measured at runtime.
 *
 * Everything is staged at 2× the host app's real sizes, because the whole scene
 * is a zoomed crop of somebody's composer. The zoom comes from intrinsic sizes,
 * never a transform: scaling the subtree would leave the slingshot's pointer
 * deltas (screen px) out of step with its physics (local px).
 *
 * The notches are *not* here — where they fall is a function of the scale, and the
 * scale is vocabulary, which belongs to the theme. See `effort-theme.ts`.
 */

/** The popover card, and the width the track has to fill inside it. */
export const CARD_WIDTH = 440;
export const CARD_PADDING = 20;
export const TRACK_WIDTH = CARD_WIDTH - CARD_PADDING * 2;
/** The knob's hit box, and the unit the physics moves. Each theme paints a
 * differently sized skin inside it — the box itself never changes, so a reskin
 * can't quietly retune the slingshot. */
export const KNOB_SIZE = 38;
/** The knob runs flush end to end — no inset, so it caps the track exactly. */
export const TRACK_TRAVEL = TRACK_WIDTH - KNOB_SIZE;

export const DEFAULT_LEVEL = 1;

export const clamp = (value: number, min: number, max: number) =>
  Math.min(Math.max(value, min), max);

/** Both performed variants score in percent and only convert to track-pixels at
 * the last moment — the dial is the readout, not the unit of measurement. */
export const percentToX = (percent: number) => (clamp(percent, 0, 100) / 100) * TRACK_TRAVEL;
```

### `/effort-theme.ts`

```ts
import { clamp, TRACK_TRAVEL } from "./effort-scale";

/**
 * A theme is the *host app* the picker is pretending to live inside: its surfaces
 * and its dial. It is orthogonal to the variant, because the variant owns the
 * physics and the theme owns the paint.
 *
 * Every value below was read off the real product with the browser open, then
 * doubled: the scene is staged at 2× (see `effort-scale.ts`), so a 12px radius
 * over there is a 24px radius here.
 */
export type EffortTheme = "chatgpt" | "claude";

/** Positional — the index is the level. Two names minimum so there is always a
 * `levels[0]` to fall back on, and so a notch gap is never a division by zero. */
type LevelNames = readonly [string, string, ...string[]];

/** The rungs, in order. Both host apps run Codex's scale: a reskin changes what
 * the dial is made of, not what it can say — and `Ultra` has to mean the end of
 * the track in every skin, or the verdict names a rung the knob doesn't land on.
 * Still a theme token, so a third host app can bring its own words. */
const LEVELS = ["Light", "Medium", "High", "Extra High", "Ultra"] as const;

/** The knob's paint. Sized independently of `KNOB_SIZE`: the hit box drives the
 * physics, this is only what you see riding in it. */
interface KnobSkin {
  width: number;
  height: number;
  radius: number;
  className: string;
}

interface ThemeTokens {
  levels: LevelNames;
  /** The cropped app the composer sits in. */
  frame: string;
  /** The popover shell — shared by all three variants, so whichever take you
   * perform, it lands on the host app's own surface. */
  card: string;
  /** Chrome text, in descending loudness. */
  text: string;
  muted: string;
  faint: string;
  /** Focus ring, matched to the host app's accent. */
  ring: string;
  /** The dial: trough, the bar filling it, notches, knob. */
  troughHeight: number;
  troughRadius: number;
  trough: string;
  /** Colour ramp under the knob, low end to high end. Both stops can be the same
   * colour when the app fills its track flatly rather than by temperature. */
  fill: readonly [string, string];
  dot: string;
  /** The top notch gets its own colour: the one rung the app wants you to notice. */
  dotTop: string;
  knob: KnobSkin;
}

export const EFFORT_THEMES = {
  chatgpt: {
    card: "rounded-[28px] border border-white/10 bg-neutral-800/95 shadow-2xl shadow-black/60",
    dot: "bg-white/45",
    dotTop: "bg-white/45",
    faint: "text-neutral-500",
    // Codex blue for most of the track, shifting into violet over the last
    // stretch — the fill reads as a temperature before any label does. It tops
    // out at violet, not white: white belongs to the knob and the band.
    fill: ["#3b82f6", "#a855f7"],
    frame: "bg-neutral-950 text-neutral-100",
    // A circle exactly as wide as its hit box: the Codex knob rides proud of the
    // trough and caps the track at either end.
    knob: { className: "bg-white shadow-lg", height: 38, radius: 19, width: 38 },
    levels: LEVELS,
    muted: "text-neutral-400",
    ring: "focus-visible:ring-violet-500/60",
    text: "text-neutral-100",
    trough: "bg-neutral-700/60",
    troughHeight: 30,
    troughRadius: 15,
  },
  claude: {
    // No border and no drop shadow: the surface is drawn with a 1px inset stroke
    // (2px here) so the card's edge reads as a lit rim rather than an outline.
    card: "rounded-[24px] bg-[#2c2c2a] shadow-[inset_0_0_0_2px_rgba(255,255,255,0.08)]",
    dot: "bg-white/25",
    dotTop: "bg-[#b796ff]",
    faint: "text-white/25",
    // Flat, not a temperature: the filled stretch is just the surface lifted.
    fill: ["rgba(255,255,255,0.16)", "rgba(255,255,255,0.16)"],
    frame: "bg-[#20201f] text-[#e1e0d9]",
    // Narrower than its hit box and exactly as tall as the trough: this knob sits
    // down inside the groove rather than on top of it.
    knob: { className: "bg-[#a5a49a]", height: 40, radius: 12, width: 32 },
    levels: LEVELS,
    muted: "text-white/48",
    ring: "focus-visible:ring-[#b796ff]/60",
    text: "text-white/70",
    // The trough is *darker* than the card it sits on — a groove cut into the
    // surface, where the Codex one is a rail laid on top of it.
    trough: "bg-[#1a1a19]",
    troughHeight: 40,
    troughRadius: 12,
  },
} satisfies Record<EffortTheme, ThemeTokens>;

export const levelCount = (theme: EffortTheme) => EFFORT_THEMES[theme].levels.length;

/** Notch spacing is a function of the theme, since the two apps cut a different
 * number of stops into the same length of track. */
const notchGap = (theme: EffortTheme) => TRACK_TRAVEL / (levelCount(theme) - 1);

export const notchX = (theme: EffortTheme, level: number) =>
  clamp(level, 0, levelCount(theme) - 1) * notchGap(theme);

export const nearestLevel = (theme: EffortTheme, x: number) =>
  clamp(Math.round(x / notchGap(theme)), 0, levelCount(theme) - 1);

/** Takes a level *index*, not a track position. */
export const labelAt = (theme: EffortTheme, index: number) => {
  const { levels } = EFFORT_THEMES[theme];
  return levels[clamp(Math.round(index), 0, levels.length - 1)] ?? levels[0];
};
```

### `/karaoke-lyrics.ts`

```ts
/** The bars the card performs. Percentage words kick the dial. */

export interface LyricWord {
  readonly text: string;
  /** Set on the number word opening an "<n> percent" phrase. Drives the dial. */
  readonly percent?: number;
  /** Position inside the line, in beats. Keeps the fill on the song's cadence. */
  readonly beat: number;
}

export interface LyricLine {
  readonly words: readonly LyricWord[];
  readonly durationBeats: number;
}

interface LineTiming {
  percent: number;
  percentWordIndex: number;
  wordBeats: number[];
  durationBeats: number;
}

const line = (text: string, timing: LineTiming): LyricLine => {
  const words = text.split(" ");
  if (words.length !== timing.wordBeats.length) {
    throw new Error(`Expected one beat cue per word in "${text}".`);
  }
  if (
    !Number.isInteger(timing.percentWordIndex) ||
    timing.percentWordIndex < 0 ||
    timing.percentWordIndex >= words.length
  ) {
    throw new Error(`Percentage cue falls outside "${text}".`);
  }
  if (!Number.isFinite(timing.percent) || timing.percent < 0 || timing.percent > 100) {
    throw new Error(`Invalid percentage cue for "${text}".`);
  }
  if (!Number.isFinite(timing.durationBeats) || timing.durationBeats <= 0) {
    throw new Error(`Invalid line duration for "${text}".`);
  }

  let previousBeat = -1;
  for (const beat of timing.wordBeats) {
    if (
      !Number.isFinite(beat) ||
      beat < 0 ||
      beat >= timing.durationBeats ||
      beat <= previousBeat
    ) {
      throw new Error(`Invalid beat cue in "${text}".`);
    }
    previousBeat = beat;
  }

  return Object.freeze({
    durationBeats: timing.durationBeats,
    words: Object.freeze(
      words.map((word, index) => {
        const beat = timing.wordBeats[index];
        if (beat === undefined) {
          throw new Error(`Missing beat cue for "${word}".`);
        }
        return index === timing.percentWordIndex
          ? { beat, percent: timing.percent, text: word }
          : { beat, text: word };
      }),
    ),
  });
};

export const LYRIC_LINES: readonly LyricLine[] = Object.freeze([
  line("This is ten percent luck", {
    durationBeats: 2,
    percent: 10,
    percentWordIndex: 2,
    wordBeats: [0, 0.35, 0.75, 1.25, 1.7],
  }),
  line("Twenty percent skill", {
    durationBeats: 2,
    percent: 20,
    percentWordIndex: 0,
    wordBeats: [0, 0.75, 1.35],
  }),
  line("Fifteen percent concentrated power of will", {
    durationBeats: 4,
    percent: 15,
    percentWordIndex: 0,
    wordBeats: [0, 0.55, 1.15, 2.05, 2.7, 3.25],
  }),
  line("Five percent pleasure", {
    durationBeats: 2,
    percent: 5,
    percentWordIndex: 0,
    wordBeats: [0, 0.7, 1.35],
  }),
  line("Fifty percent pain", {
    durationBeats: 2,
    percent: 50,
    percentWordIndex: 0,
    wordBeats: [0, 0.7, 1.35],
  }),
  line("And a hundred percent reason to remember the name", {
    durationBeats: 4,
    percent: 100,
    percentWordIndex: 2,
    wordBeats: [0, 0.4, 0.85, 1.3, 1.75, 2.15, 2.55, 3, 3.45],
  }),
]);

export type FlatWord = LyricWord & {
  readonly lineIndex: number;
  readonly cueBeat: number;
};

const buildSchedule = (lines: readonly LyricLine[]) => {
  const words: FlatWord[] = [];
  let elapsedBeats = 0;

  for (const [lineIndex, lyricLine] of lines.entries()) {
    for (const word of lyricLine.words) {
      words.push({ ...word, cueBeat: elapsedBeats + word.beat, lineIndex });
    }
    elapsedBeats += lyricLine.durationBeats;
  }

  return { totalBeats: elapsedBeats, words: Object.freeze(words) };
};

const schedule = buildSchedule(LYRIC_LINES);

/** The verse as one immutable stream — the performance is scheduled off this. */
export const FLAT_WORDS = schedule.words;
export const VERSE_BEATS = schedule.totalBeats;

export const averagePercent = (percents: number[]) =>
  percents.length === 0
    ? 0
    : Math.round(percents.reduce((total, percent) => total + percent, 0) / percents.length);
```

### `/karaoke-track.tsx`

```tsx
"use client";

import { useCallback, useEffect, useRef, useState } from "react";
import { animate, AnimatePresence, motion, useReducedMotion } from "motion/react";
import { RotateCcwIcon, ZapIcon } from "lucide-react";

import type { MotionValue } from "motion/react";

import type { EffortTheme } from "./effort-theme";

import { DialTrough, knobBoxStyle, KnobSkin, Sparks } from "./effort-dial";
import { KNOB_SIZE, percentToX, TRACK_TRAVEL, TRACK_WIDTH } from "./effort-scale";
import { EFFORT_THEMES, labelAt, nearestLevel, notchX } from "./effort-theme";
import { averagePercent, FLAT_WORDS, LYRIC_LINES, VERSE_BEATS } from "./karaoke-lyrics";

/** The original track sits at 85 BPM. Every cue below lands on that grid. */
const BEAT_MS = 60_000 / 85;
const COUNTDOWN_BEATS = 3;
const COUNT_IN_MS = BEAT_MS * COUNTDOWN_BEATS;
/** Beat between the last word and the dial committing. */
const COMMIT_DELAY_MS = 700;

const DIAL_SPRING = { damping: 24, stiffness: 260, type: "spring" } as const;
const COMMIT_SPRING = { damping: 18, stiffness: 180, type: "spring" } as const;

interface Burst {
  id: number;
  percent: number;
}
type CountdownCount = 3 | 2 | 1;

type TakeState =
  | { status: "countdown"; count: CountdownCount }
  | { status: "singing"; matched: number }
  | { status: "complete"; level: number };

const promptForCountdown = (count: CountdownCount) => (count === 1 ? "Let's go!" : "You ready?!");

/**
 * The bloom that escapes the lyric panel. It stays close to the edge — a rim of
 * light leaking out from behind the verdict, not a flare washing over the card.
 */
const CompletionGlow = ({ reduceMotion }: { reduceMotion: boolean }) => (
  <motion.div
    aria-hidden
    className="pointer-events-none absolute -inset-2 z-0"
    initial={{ opacity: 0 }}
    animate={{ opacity: 1 }}
    exit={{ opacity: 0 }}
  >
    <motion.span
      className="absolute inset-2 rounded-2xl bg-violet-500/40 blur-xl"
      initial={{ opacity: 0 }}
      animate={reduceMotion ? { opacity: [0, 0.3, 0.14] } : { opacity: [0, 0.7, 0.22] }}
      transition={{ duration: reduceMotion ? 0.5 : 1.1, ease: "easeOut", times: [0, 0.24, 1] }}
    />
  </motion.div>
);

/** Sparse, panel-bound — a few catches of light, not a firework. */
const FINALE_GLINTS = [
  { delay: 0.1, x: -148, y: -16 },
  { delay: 0.18, x: -92, y: 16 },
  { delay: 0.28, x: 78, y: -18 },
  { delay: 0.36, x: 142, y: 12 },
];

const FinaleGlints = () => (
  <>
    {FINALE_GLINTS.map((glint) => {
      const destination = `translate(calc(-50% + ${glint.x}px), calc(-50% + ${glint.y}px))`;
      return (
        <motion.span
          key={`${glint.x}-${glint.y}`}
          className="absolute top-1/2 left-1/2 size-4 mix-blend-screen"
          initial={{ opacity: 0, transform: "translate(-50%, -50%) scale(0) rotate(0deg)" }}
          animate={{
            opacity: [0, 0.85, 0],
            transform: [
              "translate(-50%, -50%) scale(0) rotate(0deg)",
              `${destination} scale(1) rotate(45deg)`,
              `${destination} scale(0.2) rotate(90deg)`,
            ],
          }}
          transition={{ delay: glint.delay, duration: 0.66, ease: "easeOut", times: [0, 0.45, 1] }}
        >
          <span className="absolute top-1/2 inset-x-0 h-px bg-violet-100 shadow-[0_0_6px_rgba(221,214,254,0.8)]" />
          <span className="absolute left-1/2 inset-y-0 w-px bg-violet-100 shadow-[0_0_6px_rgba(221,214,254,0.8)]" />
        </motion.span>
      );
    })}
  </>
);

/**
 * Everything else is clipped to the lyric panel: the verse burns off, the panel
 * catches the light for a beat, and a single specular sweep crosses it.
 */
const CompletionShimmer = () => (
  <motion.div
    aria-hidden
    className="pointer-events-none absolute inset-0 z-10 overflow-hidden rounded-2xl"
    initial={{ opacity: 0 }}
    animate={{ opacity: 1 }}
    exit={{ opacity: 0 }}
  >
    <motion.span
      className="absolute inset-0 bg-[radial-gradient(ellipse_at_center,rgba(196,181,253,0.32),rgba(139,92,246,0.18)_45%,transparent_75%)] mix-blend-screen"
      initial={{ opacity: 0 }}
      animate={{ opacity: [0, 1, 0.35, 0] }}
      transition={{ duration: 1.1, ease: "easeOut", times: [0, 0.18, 0.55, 1] }}
    />
    <motion.span
      className="absolute inset-px rounded-[15px] border border-violet-300/50 shadow-[inset_0_0_20px_rgba(167,139,250,0.3)]"
      initial={{ opacity: 0 }}
      animate={{ opacity: [0, 1, 0.4, 0] }}
      transition={{ duration: 1.4, ease: "easeOut", times: [0, 0.14, 0.55, 1] }}
    />
    <motion.span
      className="absolute top-1/2 left-1/2 h-px w-2/3 origin-center bg-gradient-to-r from-transparent via-violet-100 to-transparent shadow-[0_0_10px_rgba(196,181,253,0.7)] mix-blend-screen"
      initial={{ opacity: 0, transform: "translate(-50%, -50%) scaleX(0)" }}
      animate={{
        opacity: [0, 0.7, 0],
        transform: [
          "translate(-50%, -50%) scaleX(0)",
          "translate(-50%, -50%) scaleX(1)",
          "translate(-50%, -50%) scaleX(0.7)",
        ],
      }}
      transition={{ duration: 0.8, ease: "easeOut", times: [0, 0.3, 1] }}
    />
    <motion.span
      className="absolute top-1/2 left-1/2 h-32 w-16 bg-gradient-to-r from-transparent via-white/35 to-transparent blur-md mix-blend-screen"
      initial={{ opacity: 0, transform: "translate(-360%, -50%) rotate(14deg)" }}
      animate={{
        opacity: [0, 1, 1, 0],
        transform: [
          "translate(-360%, -50%) rotate(14deg)",
          "translate(-200%, -50%) rotate(14deg)",
          "translate(120%, -50%) rotate(14deg)",
          "translate(300%, -50%) rotate(14deg)",
        ],
      }}
      transition={{ duration: 1.1, ease: "easeInOut", times: [0, 0.2, 0.78, 1] }}
    />
    <FinaleGlints />
  </motion.div>
);

const wordClass = (isPercent: boolean, done: boolean) => {
  if (isPercent) {
    return done ? "text-violet-400" : "text-violet-400/30";
  }
  return done ? "text-neutral-100" : "text-neutral-600";
};

/** Words landed so far — the whole verse once the take has committed. */
const matchedWords = (take: TakeState) => {
  if (take.status === "singing") {
    return take.matched;
  }
  if (take.status === "complete") {
    return FLAT_WORDS.length;
  }
  return 0;
};

interface MotionProps {
  reduceMotion: boolean;
}

const AgainButton = ({ onClick }: { onClick: () => void }) => (
  <motion.button
    type="button"
    onClick={onClick}
    initial={{ opacity: 0, scale: 0.9 }}
    animate={{ opacity: 1, scale: 1 }}
    exit={{ opacity: 0, scale: 0.9 }}
    transition={{ duration: 0.16 }}
    className="flex shrink-0 items-center gap-1.5 rounded-full border border-violet-500/60 bg-violet-500/10 px-3 py-1.5 text-[13px] text-neutral-100 outline-none focus-visible:ring-2 focus-visible:ring-violet-500/60"
  >
    <RotateCcwIcon className="size-3.5 text-violet-300" />
    Again
    <ZapIcon className="size-3.5 text-violet-300" />
  </motion.button>
);

/** The counter scrolls with the verse: the bar you just finished lifts away
 * while the one you're on rises to take its place. */
const BarCounter = ({ lineIndex, reduceMotion }: MotionProps & { lineIndex: number }) => (
  <motion.span
    initial={{ opacity: 0 }}
    animate={{ opacity: 1 }}
    exit={{ opacity: 0 }}
    className="flex shrink-0 items-center gap-1 text-[13px] tabular-nums text-neutral-400"
  >
    Bar
    <span className="relative flex h-4 w-2 items-center justify-center overflow-hidden">
      <AnimatePresence mode="popLayout" initial={false}>
        <motion.span
          key={lineIndex}
          initial={reduceMotion ? { opacity: 0 } : { opacity: 0, y: 9 }}
          animate={{ opacity: 1, y: 0 }}
          exit={reduceMotion ? { opacity: 0 } : { opacity: 0, y: -9 }}
          transition={{ duration: reduceMotion ? 0.1 : 0.22, ease: [0.4, 0, 0.2, 1] }}
          className="absolute inset-0 flex items-center justify-center"
        >
          {lineIndex + 1}
        </motion.span>
      </AnimatePresence>
    </span>
    of {LYRIC_LINES.length}
  </motion.span>
);

const Countdown = ({ count, reduceMotion }: MotionProps & { count: CountdownCount }) => (
  <div className="relative z-20 flex w-full items-center justify-between gap-4">
    <AnimatePresence mode="popLayout" initial={false}>
      <motion.p
        key={promptForCountdown(count)}
        initial={
          reduceMotion ? { opacity: 0 } : { opacity: 0, transform: "translateY(8px) scale(0.96)" }
        }
        animate={{ opacity: 1, transform: "translateY(0) scale(1)" }}
        exit={
          reduceMotion ? { opacity: 0 } : { opacity: 0, transform: "translateY(-8px) scale(1.03)" }
        }
        transition={{ duration: reduceMotion ? 0.1 : 0.2, ease: "easeOut" }}
        className="text-left text-[17px] font-medium text-neutral-100"
      >
        {promptForCountdown(count)}
      </motion.p>
    </AnimatePresence>

    <span className="relative flex h-8 min-w-5 shrink-0 items-center justify-end overflow-hidden">
      <AnimatePresence mode="popLayout" initial={false}>
        <motion.span
          key={count}
          aria-label={`Starts in ${count}`}
          initial={
            reduceMotion
              ? { opacity: 0 }
              : { opacity: 0, transform: "translateY(-7px) scale(1.15)" }
          }
          animate={{ opacity: 1, transform: "translateY(0) scale(1)" }}
          exit={
            reduceMotion ? { opacity: 0 } : { opacity: 0, transform: "translateY(7px) scale(0.9)" }
          }
          transition={{ duration: reduceMotion ? 0.1 : 0.18, ease: "easeOut" }}
          className="block min-w-5 text-right text-[18px] leading-none font-semibold tabular-nums text-violet-300"
        >
          {count}
        </motion.span>
      </AnimatePresence>
    </span>
  </div>
);

/** Bars scroll like a teleprompter: the outgoing line lifts, blurs and fades
 * while the next one rises into focus underneath it. */
const Verse = ({
  lineIndex,
  matched,
  reduceMotion,
}: MotionProps & { lineIndex: number; matched: number }) => {
  const line = LYRIC_LINES[lineIndex];
  const lineStart = FLAT_WORDS.findIndex((word) => word.lineIndex === lineIndex);

  return (
    <motion.p
      initial={reduceMotion ? { opacity: 0 } : { filter: "blur(5px)", opacity: 0, y: 14 }}
      animate={{ filter: "blur(0px)", opacity: 1, y: 0 }}
      exit={reduceMotion ? { opacity: 0 } : { filter: "blur(5px)", opacity: 0, y: -14 }}
      transition={{ duration: reduceMotion ? 0.1 : 0.22, ease: [0.4, 0, 0.2, 1] }}
      // Pinned to the panel rather than laid out in it: popLayout yanks the
      // outgoing bar out of flow, and a shrink-wrapped bar re-wraps mid-exit.
      // Same width in, same width out — a bar that wraps, wraps the whole time.
      className="absolute inset-x-4 inset-y-0 z-20 flex flex-wrap content-center items-center justify-center gap-x-1.5 text-center text-[15px] font-medium"
    >
      {line?.words.map((word, index) => (
        <span
          key={`${lineIndex}-${lineStart + index}`}
          className={wordClass(word.percent !== undefined, lineStart + index < matched)}
        >
          {word.text}
        </span>
      ))}
    </motion.p>
  );
};

/** The verse gives way to the verdict. It arrives with a flare behind it,
 * because six bars of arithmetic deserve at least a bit of ceremony. */
const Verdict = ({ label, reduceMotion }: MotionProps & { label: string }) => (
  <motion.div
    initial={reduceMotion ? { opacity: 0 } : { opacity: 0, scale: 0.9 }}
    animate={{ opacity: 1, scale: 1 }}
    transition={reduceMotion ? { duration: 0.1 } : { damping: 20, stiffness: 320, type: "spring" }}
    className="relative z-20 flex items-center justify-center"
  >
    {!reduceMotion && (
      <motion.span
        aria-hidden
        className="absolute size-3 rounded-full bg-violet-500/60 blur-md"
        initial={{ opacity: 0.9, scale: 0.4 }}
        animate={{ opacity: [0.9, 0.35, 0.18], scale: [0.4, 14, 11] }}
        transition={{ duration: 0.9, ease: "easeOut" }}
      />
    )}
    <p className="relative flex items-baseline gap-1.5 text-[17px] font-medium">
      <span className="text-neutral-400">Landed on</span>
      <span className="text-violet-300">{label}</span>
    </p>
  </motion.div>
);

interface KaraokeCardProps {
  knobX: MotionValue<number>;
  theme: EffortTheme;
}

/**
 * You don't set this dial. You perform for it — or rather, it performs for you.
 *
 * The whole take is on rails: a count-in, then the verse fills itself in word by
 * word, each percentage kicking the needle and throwing sparks. Nothing commits
 * until the last bar lands, and the effort you end up with is the *average* of
 * every percentage in the verse — perform the entire thing and you still land
 * somewhere unremarkable.
 */
export const KaraokeCard = ({ knobX, theme }: KaraokeCardProps) => {
  const [bursts, setBursts] = useState<Burst[]>([]);
  const [take, setTake] = useState<TakeState>({ count: 3, status: "countdown" });
  const reduceMotion = Boolean(useReducedMotion());

  const burstId = useRef(0);
  const timers = useRef<number[]>([]);
  const reduceMotionRef = useRef(reduceMotion);

  useEffect(() => {
    reduceMotionRef.current = reduceMotion;
  }, [reduceMotion]);

  const clearTimers = useCallback(() => {
    for (const timer of timers.current) {
      window.clearTimeout(timer);
    }
    timers.current = [];
  }, []);
  const after = useCallback((ms: number, run: () => void) => {
    timers.current.push(window.setTimeout(run, ms));
  }, []);

  // The line on screen is the one the *last landed word* belongs to, not the one
  // the next word belongs to. Reading ahead flipped the line the instant a bar's
  // final word landed, so that word was never seen lit — every bar looked like it
  // skipped its last word, and the closing bar was cut off entirely by the commit.
  const matched = matchedWords(take);
  const currentLineIndex =
    matched === 0 ? 0 : (FLAT_WORDS[matched - 1]?.lineIndex ?? LYRIC_LINES.length - 1);

  const run = useCallback(() => {
    clearTimers();
    setBursts([]);
    setTake({ count: 3, status: "countdown" });
    if (reduceMotionRef.current) {
      knobX.set(0);
    } else {
      void animate(knobX, 0, DIAL_SPRING);
    }

    after(BEAT_MS, () => setTake({ count: 2, status: "countdown" }));
    after(BEAT_MS * 2, () => setTake({ count: 1, status: "countdown" }));
    after(COUNT_IN_MS, () => setTake({ matched: 0, status: "singing" }));

    // Cues are song beats, not a uniform word ticker. Short bars breathe while the
    // dense final bar runs faster, matching the recorded cadence without drift.
    for (const [index, word] of FLAT_WORDS.entries()) {
      after(COUNT_IN_MS + word.cueBeat * BEAT_MS, () => {
        setTake({ matched: index + 1, status: "singing" });
        if (word.percent === undefined) {
          return;
        }

        const landed = word.percent;
        if (reduceMotionRef.current) {
          knobX.set(percentToX(landed));
          return;
        }
        void animate(knobX, percentToX(landed), DIAL_SPRING);
        burstId.current += 1;
        const burst = { id: burstId.current, percent: landed };
        setBursts((current) => [...current, burst]);
        after(800, () => setBursts((current) => current.filter((item) => item.id !== burst.id)));
      });
    }

    after(COUNT_IN_MS + VERSE_BEATS * BEAT_MS + COMMIT_DELAY_MS, () => {
      const average = averagePercent(
        FLAT_WORDS.flatMap((word) => (word.percent === undefined ? [] : [word.percent])),
      );
      // The average is a percentage, but the dial only has notches — so the take
      // settles on the nearest one. That rounding IS the joke: perform six bars of
      // precise percentages and the machine shrugs them into "Medium".
      const level = nearestLevel(theme, percentToX(average));
      setTake({ level, status: "complete" });
      if (reduceMotionRef.current) {
        knobX.set(notchX(theme, level));
      } else {
        void animate(knobX, notchX(theme, level), COMMIT_SPRING);
      }

      // One last burst, on the notch it actually landed on.
      if (reduceMotionRef.current) {
        return;
      }
      burstId.current += 1;
      const finale = { id: burstId.current, percent: (notchX(theme, level) / TRACK_TRAVEL) * 100 };
      setBursts((current) => [...current, finale]);
      after(900, () => setBursts((current) => current.filter((item) => item.id !== finale.id)));
    });
  }, [after, clearTimers, knobX, theme]);

  useEffect(() => {
    const frame = requestAnimationFrame(run);
    return () => {
      cancelAnimationFrame(frame);
      clearTimers();
    };
  }, [run, clearTimers]);

  return (
    <div className={`relative p-5 ${EFFORT_THEMES[theme].card}`}>
      <div className="relative mb-4 flex h-8 items-center justify-between gap-3">
        <p className="text-[15px] font-medium text-neutral-100">Reasoning effort</p>

        {/* The right slot carries the state of the take: how far in you are, and
            only once it lands, the way to run it back. */}
        <AnimatePresence mode="wait" initial={false}>
          {take.status === "complete" && <AgainButton key="again" onClick={run} />}
          {take.status === "singing" && (
            <BarCounter key="progress" lineIndex={currentLineIndex} reduceMotion={reduceMotion} />
          )}
        </AnimatePresence>
      </div>

      <div className="relative mb-4 h-16">
        <AnimatePresence>
          {take.status === "complete" && <CompletionGlow reduceMotion={reduceMotion} />}
        </AnimatePresence>

        <div
          aria-live="polite"
          className="relative z-10 flex h-full items-center justify-center overflow-hidden rounded-2xl bg-neutral-900/70 px-4"
        >
          <AnimatePresence>
            {take.status === "complete" && !reduceMotion && <CompletionShimmer />}
          </AnimatePresence>

          <AnimatePresence mode="popLayout">
            {take.status === "countdown" && (
              <Countdown key="countdown" count={take.count} reduceMotion={reduceMotion} />
            )}
            {take.status === "singing" && (
              <Verse
                key={currentLineIndex}
                lineIndex={currentLineIndex}
                matched={matched}
                reduceMotion={reduceMotion}
              />
            )}
            {take.status === "complete" && (
              <Verdict
                key="landed"
                label={labelAt(theme, take.level)}
                reduceMotion={reduceMotion}
              />
            )}
          </AnimatePresence>
        </div>
      </div>

      <div className="relative" style={{ height: KNOB_SIZE, width: TRACK_WIDTH }}>
        <DialTrough knobX={knobX} theme={theme} />

        <AnimatePresence>
          {!reduceMotion &&
            bursts.map((burst) => <Sparks key={burst.id} percent={burst.percent} />)}
        </AnimatePresence>

        <motion.div aria-hidden className="absolute" style={{ ...knobBoxStyle(theme), x: knobX }}>
          <KnobSkin theme={theme} />
        </motion.div>
      </div>
    </div>
  );
};
```

### `/preview.tsx`

```tsx
"use client";

import { useState } from "react";
import { ChevronDownIcon } from "lucide-react";

import type { EffortTheme, EffortVariant } from "./effort-picker";

import { EffortPicker } from "./effort-picker";

const VARIANTS: { value: EffortVariant; label: string }[] = [
  { label: "Slingshot", value: "slingshot" },
  { label: "Karaoke", value: "karaoke" },
  { label: "Curls", value: "curls" },
];

const THEMES: { value: EffortTheme; label: string }[] = [
  { label: "ChatGPT", value: "chatgpt" },
  { label: "Claude", value: "claude" },
];

const isVariant = (value: string): value is EffortVariant =>
  VARIANTS.some((variant) => variant.value === value);

const isTheme = (value: string): value is EffortTheme =>
  THEMES.some((theme) => theme.value === value);

interface PickerProps<T extends string> {
  label: string;
  value: T;
  options: { value: T; label: string }[];
  onChange: (value: string) => void;
}

const Picker = <T extends string>({ label, value, options, onChange }: PickerProps<T>) => (
  <span className="relative flex items-center rounded-full border border-white/10 bg-neutral-900">
    <select
      aria-label={label}
      value={value}
      onChange={(event) => onChange(event.target.value)}
      className="appearance-none bg-transparent py-1.5 pr-7 pl-3 text-xs text-neutral-400 outline-none"
    >
      {options.map((option) => (
        <option key={option.value} value={option.value}>
          {option.label}
        </option>
      ))}
    </select>
    <ChevronDownIcon className="pointer-events-none absolute right-2.5 size-3.5 text-neutral-500" />
  </span>
);

const Preview = () => {
  const [variant, setVariant] = useState<EffortVariant>("slingshot");
  const [theme, setTheme] = useState<EffortTheme>("chatgpt");

  return (
    <div className="relative h-screen w-full bg-neutral-950">
      <span className="absolute top-4 left-4 z-10 flex items-center gap-2">
        <Picker
          label="Variation"
          value={variant}
          options={VARIANTS}
          onChange={(value) => {
            if (isVariant(value)) {
              setVariant(value);
            }
          }}
        />
        <Picker
          label="Theme"
          value={theme}
          options={THEMES}
          onChange={(value) => {
            if (isTheme(value)) {
              setTheme(value);
            }
          }}
        />
      </span>

      <EffortPicker variant={variant} theme={theme} />
    </div>
  );
};

export default Preview;
```

### `/slingshot-track.tsx`

```tsx
"use client";

import { useEffect, useId, useRef, useState } from "react";
import { animate, motion, useMotionValue, useMotionValueEvent, useTransform } from "motion/react";
import { ChevronRightIcon, CircleHelpIcon, ZapIcon } from "lucide-react";

import type { MotionValue } from "motion/react";

import type { EffortTheme } from "./effort-theme";

import { DialTrough, knobBoxStyle, KnobSkin, LevelLabel } from "./effort-dial";
import { clamp, DEFAULT_LEVEL, KNOB_SIZE, TRACK_TRAVEL, TRACK_WIDTH } from "./effort-scale";
import { EFFORT_THEMES, labelAt, levelCount, nearestLevel, notchX } from "./effort-theme";

/**
 * How far past either cap the band stretches. Tuned by simulating the landing
 * distribution: below ~100px a shot from the bottom notch can never reach the
 * top ones, and every notch has to stay winnable from every other.
 */
const MAX_OVERPULL = 140;
/** Launch speed (px/s) earned per px of stretch. */
const LAUNCH_GAIN = 12;
/** Exponential velocity decay, per second. */
const DRAG = 3.4;
/** Fraction of speed kept after hitting a wall. */
const RESTITUTION = 0.62;
/** Below this speed the knob gives up and snaps to the nearest notch. */
const SETTLE_SPEED = 55;
/** Escape hatch: nothing flies forever. */
const MAX_FLIGHT_SECONDS = 6;
/** A stretch smaller than this is a fidget, not a shot. */
const MIN_STRETCH = 4;
/** The stretch that reads as "fully wound up", for the band's colour. */
const FULL_STRETCH = 170;
/** The speed that reads as "flat out", for the knob's stretch. */
const FULL_SPEED = 1800;
/** Blur radius at full speed, in px. Applied along X only — motion blur smears
 * along the direction of travel; an even blur just looks out of focus. */
const MAX_BLUR = 7;
/** Below this speed the blur filter is dropped entirely rather than left at ~0,
 * so a resting knob keeps its crisp edge and costs nothing to composite. */
const BLUR_CUTOFF = 0.03;

const SETTLE_SPRING = { damping: 26, stiffness: 420, type: "spring" } as const;
const IMPACT_SPRING = { damping: 18, stiffness: 500, type: "spring" } as const;
const RELEASE_SPRING = { damping: 30, stiffness: 700, type: "spring" } as const;

export type TrackPhase = "idle" | "pulling" | "flying";

const ARROW_DELTA = new Map([
  ["ArrowLeft", -1],
  ["ArrowRight", 1],
]);

/** Past either cap the band goes taut rather than snapping — an asymptote, so
 * there is always runway to pull against even when sitting on an end notch. */
const overpull = (raw: number) => {
  if (raw < 0) {
    return -MAX_OVERPULL * Math.tanh(-raw / MAX_OVERPULL);
  }
  if (raw > TRACK_TRAVEL) {
    return TRACK_TRAVEL + MAX_OVERPULL * Math.tanh((raw - TRACK_TRAVEL) / MAX_OVERPULL);
  }
  return raw;
};

interface PullOrigin {
  anchor: number;
  pointer: number;
}

interface SlingshotTrackProps {
  /** Knob offset in px, owned by the shell so the chrome can follow it. */
  knobX: MotionValue<number>;
  theme: EffortTheme;
  /** Fires whenever the knob starts or stops being a projectile. */
  onPhaseChange?: (phase: TrackPhase) => void;
}

/**
 * An effort slider you cannot place — only fire. Haul the knob back either way
 * against the band and let go: it launches, ricochets off the ends of the track,
 * and settles wherever it ran out of momentum. Aiming is a suggestion.
 */
export const SlingshotTrack = ({ knobX, theme, onPhaseChange }: SlingshotTrackProps) => {
  const tokens = EFFORT_THEMES[theme];
  const [phase, setPhase] = useState<TrackPhase>("idle");

  const enterPhase = (next: TrackPhase) => {
    setPhase(next);
    onPhaseChange?.(next);
  };

  /** How wound up the band is, 0–1. Lights the band — but never deforms the
   * knob: a knob that squashes while you haul on it reads as slack, not loaded. */
  const tension = useMotionValue(0);
  /** Speed, 0–1, while the knob is a projectile. Stretches it along its travel. */
  const speed = useMotionValue(0);
  /** A spike on each wall hit, kept separate so impacts don't light up the band. */
  const impact = useMotionValue(0);
  /** 0 = left edge, 1 = right edge: which wall the knob is being crushed against. */
  const knobOrigin = useMotionValue(0.5);
  /** Where the band is pinned: the notch the knob is being hauled off of. */
  const anchorX = useMotionValue(notchX(theme, DEFAULT_LEVEL));

  const velocity = useRef(0);
  const frame = useRef(0);
  const pullOrigin = useRef<PullOrigin | null>(null);

  useEffect(() => () => cancelAnimationFrame(frame.current), []);

  const settle = (x: number) => {
    const level = nearestLevel(theme, x);
    velocity.current = 0;
    enterPhase("idle");
    void animate(knobX, notchX(theme, level), SETTLE_SPRING);
    void animate(speed, 0, SETTLE_SPRING);
    knobOrigin.set(0.5);
  };

  /**
   * Harder hits squash harder — a wall tap shouldn't deform it like a wall slam.
   * The origin snaps to the contact edge so the knob flattens *against* the wall
   * instead of shrinking away from it on both sides.
   */
  const hitWall = (collisionSpeed: number, wall: "left" | "right") => {
    knobOrigin.set(wall === "left" ? 0 : 1);
    impact.set(clamp(collisionSpeed / FULL_SPEED, 0.3, 1));
    void animate(impact, 0, IMPACT_SPRING);
  };

  /** Stretch is signed: haul left, it fires right; haul right, it fires left. */
  const launch = (stretch: number) => {
    velocity.current = stretch * LAUNCH_GAIN;
    enterPhase("flying");
    void animate(tension, 0, RELEASE_SPRING);

    let last = performance.now();
    let elapsed = 0;

    const step = (now: number) => {
      // Clamped so a backgrounded tab doesn't resume with one enormous step and
      // tunnel the knob straight through a wall.
      const dt = Math.min((now - last) / 1000, 1 / 30);
      last = now;
      elapsed += dt;

      let v = velocity.current;
      let x = knobX.get() + v * dt;
      v *= Math.exp(-DRAG * dt);

      // Reflect only off a wall the knob is actually moving into. A shot fired
      // from the overpull zone starts outside the track and has to be allowed to
      // fly home, not bounce off the cap it's launching through.
      if (x < 0 && v < 0) {
        x = -x;
        hitWall(Math.abs(v), "left");
        v = -v * RESTITUTION;
      } else if (x > TRACK_TRAVEL && v > 0) {
        x = 2 * TRACK_TRAVEL - x;
        hitWall(Math.abs(v), "right");
        v = -v * RESTITUTION;
      }

      knobX.set(x);
      speed.set(clamp(Math.abs(v) / FULL_SPEED, 0, 1));
      velocity.current = v;

      if (Math.abs(v) < SETTLE_SPEED || elapsed > MAX_FLIGHT_SECONDS) {
        settle(x);
        return;
      }
      frame.current = requestAnimationFrame(step);
    };

    frame.current = requestAnimationFrame(step);
  };

  const handlePointerDown = (event: React.PointerEvent<HTMLButtonElement>) => {
    cancelAnimationFrame(frame.current);
    event.currentTarget.setPointerCapture(event.pointerId);
    const anchor = knobX.get();
    pullOrigin.current = { anchor, pointer: event.clientX };
    anchorX.set(anchor);
    velocity.current = 0;
    speed.set(0);
    enterPhase("pulling");
  };

  const handlePointerMove = (event: React.PointerEvent<HTMLButtonElement>) => {
    const origin = pullOrigin.current;
    if (!origin) {
      return;
    }

    const x = overpull(origin.anchor - (origin.pointer - event.clientX));

    knobX.set(x);
    tension.set(clamp(Math.abs(origin.anchor - x) / FULL_STRETCH, 0, 1));
  };

  const handlePointerUp = () => {
    const origin = pullOrigin.current;
    pullOrigin.current = null;
    if (!origin) {
      return;
    }

    const stretch = origin.anchor - knobX.get();
    if (Math.abs(stretch) < MIN_STRETCH) {
      void animate(tension, 0, RELEASE_SPRING);
      settle(knobX.get());
      return;
    }
    launch(stretch);
  };

  const handleKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>) => {
    const delta = ARROW_DELTA.get(event.key) ?? 0;
    if (delta === 0) {
      return;
    }
    // Keyboard users get the boring slider. The joke isn't worth locking them out.
    event.preventDefault();
    cancelAnimationFrame(frame.current);
    settle(notchX(theme, nearestLevel(theme, knobX.get()) + delta));
  };

  // The knob moves every frame, but the *level* changes a handful of times a
  // flight — cheap enough to mirror into React for assistive tech.
  const level = useTransform(knobX, (x) => nearestLevel(theme, x));
  const [ariaLevel, setAriaLevel] = useState(() => nearestLevel(theme, knobX.get()));
  useMotionValueEvent(level, "change", setAriaLevel);

  // The knob deforms only when it's a projectile, and the two deformations run on
  // opposite axes: speed smears it ALONG its travel (wide and flat), while a wall
  // crushes it INTO the wall (narrow and tall). The impact damps the smear as it
  // lands, otherwise the two would cancel out at exactly the moment of the hit —
  // which is the one frame the squash has to read.
  const knobScaleX = useTransform(
    [speed, impact],
    ([s, i]: number[]) => 1 + (s ?? 0) * 0.34 * (1 - (i ?? 0)) - (i ?? 0) * 0.34,
  );
  const knobScaleY = useTransform(
    [speed, impact],
    ([s, i]: number[]) => 1 - (s ?? 0) * 0.24 * (1 - (i ?? 0)) + (i ?? 0) * 0.26,
  );
  const knobTransformOrigin = useTransform(knobOrigin, (o) => `${o * 100}% 50%`);

  // Motion blur. CSS `blur()` is isotropic — it reads as out-of-focus, not fast —
  // so this is an SVG feGaussianBlur with a horizontal-only deviation, driven off
  // the same speed value. React can't re-render an attribute 60× a second cheaply,
  // so the deviation is written straight to the filter node.
  const blurId = `sling-blur-${useId().replaceAll(":", "")}`;
  const blurRef = useRef<SVGFEGaussianBlurElement>(null);
  useMotionValueEvent(speed, "change", (s) => {
    blurRef.current?.setAttribute("stdDeviation", `${(s * MAX_BLUR).toFixed(2)} 0`);
  });
  const knobFilter = useTransform(speed, (s) => (s > BLUR_CUTOFF ? `url(#${blurId})` : "none"));

  // The elastic itself: a taut line from the knob back to the notch it's being
  // hauled off of, brightening as it winds up. Without it, a pull just looks
  // like a slider being dragged.
  const bandLeft = useTransform(
    [knobX, anchorX],
    ([x, anchor]: number[]) => Math.min(x ?? 0, anchor ?? 0) + KNOB_SIZE / 2,
  );
  const bandWidth = useTransform([knobX, anchorX], ([x, anchor]: number[]) =>
    Math.abs((anchor ?? 0) - (x ?? 0)),
  );
  const bandColor = useTransform(tension, [0, 1], ["#a855f7", "#f3e8ff"]);
  const bandOpacity = useTransform(tension, [0, 0.04, 1], [0, 0.85, 1]);
  const ghostLeft = useTransform(anchorX, (x) => x + KNOB_SIZE / 2);

  return (
    <div className="relative" style={{ height: KNOB_SIZE, width: TRACK_WIDTH }}>
      <svg aria-hidden className="absolute size-0">
        <filter id={blurId} x="-50%" y="-50%" width="200%" height="200%">
          <feGaussianBlur ref={blurRef} stdDeviation="0 0" />
        </filter>
      </svg>

      <DialTrough knobX={knobX} theme={theme} />

      <motion.span
        aria-hidden
        className="absolute h-[3px] -translate-y-1/2 rounded-full"
        style={{
          backgroundColor: bandColor,
          left: bandLeft,
          opacity: bandOpacity,
          top: "50%",
          width: bandWidth,
        }}
      />

      <motion.span
        aria-hidden
        className="absolute size-4 -translate-x-1/2 -translate-y-1/2 rounded-full border border-white/60"
        style={{ left: ghostLeft, opacity: bandOpacity, top: "50%" }}
      />

      <motion.button
        type="button"
        // oxlint-disable-next-line jsx-a11y/prefer-tag-over-role -- the knob is a projectile drawn by motion, not a value an <input type="range"> could hold
        role="slider"
        aria-label="Reasoning effort"
        aria-valuemin={0}
        aria-valuemax={levelCount(theme) - 1}
        aria-valuenow={ariaLevel}
        aria-valuetext={labelAt(theme, ariaLevel)}
        className={`absolute outline-none focus-visible:ring-2 ${tokens.ring} ${
          phase === "pulling" ? "cursor-grabbing" : "cursor-grab"
        }`}
        style={{
          ...knobBoxStyle(theme),
          filter: knobFilter,
          scaleX: knobScaleX,
          scaleY: knobScaleY,
          touchAction: "none",
          transformOrigin: knobTransformOrigin,
          x: knobX,
        }}
        onPointerDown={handlePointerDown}
        onPointerMove={handlePointerMove}
        onPointerUp={handlePointerUp}
        onPointerCancel={handlePointerUp}
        onKeyDown={handleKeyDown}
      >
        <KnobSkin theme={theme} />
      </motion.button>
    </div>
  );
};

const HEADER_FADE = { duration: 0.18, ease: [0.4, 0, 0.2, 1] } as const;

interface SlingshotCardProps {
  knobX: MotionValue<number>;
  theme: EffortTheme;
}

/**
 * The popover the slingshot lives in — and the one place the two apps disagree
 * about how a control introduces itself. ChatGPT hides the ends of the scale
 * until you take hold of the knob; Claude states the level and both ends up
 * front and never moves them. Same track underneath either way.
 */
export const SlingshotCard = ({ knobX, theme }: SlingshotCardProps) => {
  const tokens = EFFORT_THEMES[theme];
  const [phase, setPhase] = useState<TrackPhase>("idle");
  // Hold the knob and the ChatGPT panel stops labelling itself and starts
  // labelling the *track* — which end buys you what. It's the only instruction
  // the thing gives.
  const winding = phase !== "idle";

  return (
    <div className={`p-5 ${tokens.card}`}>
      {theme === "claude" ? (
        // 16px between the header and the scale, 8px between the scale's labels
        // and its track — doubled, like everything else in the scene.
        <>
          <div className="mb-8 flex h-9 items-center justify-between gap-2">
            <span className={`text-[26px] ${tokens.muted}`}>
              Effort <LevelLabel knobX={knobX} theme={theme} className={tokens.text} />
            </span>
            <CircleHelpIcon aria-hidden className={`size-8 ${tokens.faint}`} />
          </div>
          <div className={`mb-4 flex items-center justify-between text-[24px] ${tokens.muted}`}>
            <span>Faster</span>
            <span>Smarter</span>
          </div>
        </>
      ) : (
        <div className="relative mb-5 h-6">
          <motion.div
            className="absolute inset-0 flex items-center justify-between"
            animate={{ opacity: winding ? 0 : 1 }}
            transition={HEADER_FADE}
          >
            <span className={`flex items-center gap-0.5 text-[15px] ${tokens.muted}`}>
              Advanced
              <ChevronRightIcon className={`size-4 ${tokens.faint}`} />
            </span>
            <ZapIcon className={`size-5 ${tokens.faint}`} />
          </motion.div>

          <motion.div
            aria-hidden={!winding}
            className={`absolute inset-0 flex items-center justify-between px-0.5 text-[15px] ${tokens.faint}`}
            animate={{ opacity: winding ? 1 : 0 }}
            transition={HEADER_FADE}
          >
            <span>Faster</span>
            <span>Smarter</span>
          </motion.div>
        </div>
      )}

      <div className="flex justify-center">
        <SlingshotTrack knobX={knobX} theme={theme} onPhaseChange={setPhase} />
      </div>
    </div>
  );
};
```

### `/use-pose-curls.ts`

```ts
"use client";

import { useCallback, useEffect, useRef, useState } from "react";

import type { NormalizedLandmark, PoseLandmarker } from "@mediapipe/tasks-vision";

/**
 * Webcam bicep-curl detector. Owns the camera stream, the MediaPipe pose model,
 * the detection loop and the skeleton overlay; hands the caller nothing but reps.
 *
 * It watches ONE arm — whichever of the two the camera has the best look at.
 * Counting both doubled the score for the same effort and made the overlay a
 * diagram; one arm is one unit of work, and the thing you're supposed to do is
 * legible from the picture without a caption.
 *
 * What it measures is not the elbow angle — it's the WRIST RISING. The angle was
 * tried twice against live sets and failed both times, the same way: a strict
 * curl faced toward the camera ends with the forearm pointing at the lens, and
 * pointing at the lens is exactly where a webcam can't measure. In image space
 * the projected forearm collapses and the 2D angle goes still; in world space the
 * lite model's z is too soft to carry it, and the 3D angle traversed as little as
 * a sixth of its real range on recorded slow reps — while fast sloppy swings,
 * which travel *across* the frame, scored fine. A rep counter that rewards
 * cheating and ignores form is a broken bit. Wrist height relative to the elbow,
 * in plain image pixels, moves through its full range on every real curl at any
 * speed and orientation, and never needs z; divided by the upper-arm length it's
 * also indifferent to how far from the camera you stand.
 *
 * A rep is a round trip with hysteresis, measured against the wrist's own recent
 * extremes rather than absolute heights — the wrist must rise REP_LIFT above its
 * recent low to fire and fall REP_LIFT below its recent high to re-arm, so
 * nothing here assumes where "arm down" hangs on any given body or framing, and
 * jitter at either end can't buzz out free reps.
 *
 * Two more things keep the reading still:
 *
 * - The tracked arm is sticky. Whoever is clearest *this frame* is the wrong
 *   subject: face a camera square on and the two arms are within noise of each
 *   other, so the winner flips every few frames — and every flip re-arms the rep
 *   latch, which is a swallowed rep and an overlay hopping between arms.
 * - Joints are averaged across frames. The lite model shivers a pixel or two on a
 *   perfectly still arm, and the overlay shows every bit of it.
 */

/** The wasm bundle + model the detector streams in on first open. Both are CDN
 * assets: a ~3MB model has no business in the page's initial payload, and the
 * card doesn't mount until the user asks for it.
 *
 * The wasm version must match the installed `@mediapipe/tasks-vision` — the two
 * halves are one build, and a loader talking to a different runtime's exports is
 * not a combination anyone tests. */
const WASM_BASE_URL = "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.35/wasm";
const MODEL_URL =
  "https://storage.googleapis.com/mediapipe-models/pose_landmarker/pose_landmarker_lite/float16/1/pose_landmarker_lite.task";

/** The model is a few MB of wasm — it's dynamically imported so it only costs
 * anything for the people who actually open this variant. */
const loadVision = () => import("@mediapipe/tasks-vision");

/** MediaPipe pose landmark indices for each arm. The detector commits to one. */
interface Arm {
  shoulder: number;
  elbow: number;
  wrist: number;
}
const ARMS: readonly Arm[] = [
  { elbow: 13, shoulder: 11, wrist: 15 },
  { elbow: 14, shoulder: 12, wrist: 16 },
];

/** Joints read below this confidence are dropped — a guessed wrist curls on its own. */
const MIN_VISIBILITY = 0.5;
/** How far the wrist has to travel, in upper-arm lengths, to score — measured
 * against its own recent extreme rather than any absolute height. A full curl
 * lifts the wrist from a forearm below the elbow to a forearm above it: around
 * two upper-arm lengths of travel, and never less than one even lazily done in a
 * cramped frame. Landmark jitter on a still arm is a few hundredths. Sat between
 * the two, closer to the noise: the bit wants every honest rep counted, and the
 * drain already punishes anyone gaming half-lifts harder than a strict counter
 * would. */
const REP_LIFT = 0.6;

/** How much clearer the other arm has to look, and for how many frames running,
 * before the detector changes subject. Roughly a fifth of a second of being
 * decisively better — enough that a real hand-off still lands quickly, far more
 * than the frame-to-frame noise that used to trigger one. */
const ARM_SWITCH_MARGIN = 0.12;
const ARM_SWITCH_FRAMES = 10;
/** Weight of the newest frame in the running joint average. Low enough to settle
 * the shiver, high enough that the overlay doesn't lag the arm it's drawn on. */
const SMOOTHING = 0.35;
/** Frames without a readable arm before the card is told it lost you. A single
 * dropped detection is a blink, not an exit — and `posed` gates the count-in. */
const POSE_LOST_FRAMES = 12;

interface Point2 {
  x: number;
  y: number;
}
/** Shoulder → elbow → wrist. The only shape any of the maths below cares about. */
interface Chain<T> {
  shoulder: T;
  elbow: T;
  wrist: T;
}

/** One arm, read for a single frame: joints for the lift and the overlay, and
 * how much of the chain the model was actually sure about. */
interface ArmPose {
  arm: Arm;
  /** The chain's *worst* joint — a crisp shoulder is no help if the wrist is a guess. */
  confidence: number;
  /** Normalized to [0,1] on each axis, in the video's own frame. */
  image: Chain<Point2>;
}

const readArm = (image: NormalizedLandmark[], arm: Arm): ArmPose | null => {
  const shoulder = image[arm.shoulder];
  const elbow = image[arm.elbow];
  const wrist = image[arm.wrist];
  if (!shoulder || !elbow || !wrist) {
    return null;
  }

  const confidence = Math.min(shoulder.visibility, elbow.visibility, wrist.visibility);
  if (confidence < MIN_VISIBILITY) {
    return null;
  }

  return { arm, confidence, image: { elbow, shoulder, wrist } };
};

/** Both arms as read this frame: the one being tracked (if it's readable) and
 * whichever is clearest. */
const readArms = (image: NormalizedLandmark[] | undefined, tracked: Arm | null) => {
  let held: ArmPose | null = null;
  let clearest: ArmPose | null = null;
  if (image) {
    for (const arm of ARMS) {
      const reading = readArm(image, arm);
      if (!reading) {
        continue;
      }
      if (arm === tracked) {
        held = reading;
      }
      if (!clearest || reading.confidence > clearest.confidence) {
        clearest = reading;
      }
    }
  }
  return { clearest, held };
};

/** The other arm, if it's decisively clearer than the one being tracked. */
const pickContender = (clearest: ArmPose | null, held: ArmPose | null, tracked: Arm | null) =>
  clearest &&
  clearest.arm !== tracked &&
  (!held || clearest.confidence > held.confidence + ARM_SWITCH_MARGIN)
    ? clearest
    : null;

const mix = (previous: number, next: number) => previous + (next - previous) * SMOOTHING;
const blendPoint = (a: Point2, b: Point2): Point2 => ({ x: mix(a.x, b.x), y: mix(a.y, b.y) });

const blendPose = (previous: ArmPose, next: ArmPose): ArmPose => ({
  ...next,
  image: {
    elbow: blendPoint(previous.image.elbow, next.image.elbow),
    shoulder: blendPoint(previous.image.shoulder, next.image.shoulder),
    wrist: blendPoint(previous.image.wrist, next.image.wrist),
  },
});

/**
 * How far the wrist rides above the elbow, in upper-arm lengths. Positive is up:
 * an arm hanging extended sits near -1, a finished curl near +1. `aspect`
 * un-squashes the normalized x axis so the upper-arm length is measured in real
 * proportions — landmarks are normalized per-axis, and on a 16:9 frame a
 * horizontal upper arm would otherwise read barely half as long as a vertical
 * one and double the lift it's dividing into.
 */
const wristLift = ({ shoulder, elbow, wrist }: Chain<Point2>, aspect: number): number | null => {
  const upperArm = Math.hypot((shoulder.x - elbow.x) * aspect, shoulder.y - elbow.y);
  if (upperArm === 0) {
    return null;
  }
  // Screen y grows downward, so "above the elbow" is the negated difference.
  return (elbow.y - wrist.y) / upperArm;
};

/**
 * The overlay: the tracked arm and nothing else — shoulder, elbow, wrist, lit in
 * violet, drawn in the video's intrinsic pixels. Drawing only the arm being
 * scored means the picture answers "what is this thing watching?" on its own.
 * The canvas and the video carry the same aspect and the same CSS mirror, so the
 * drawing stays registered on the body without any maths here.
 */
const drawArm = (
  canvas: HTMLCanvasElement | null,
  video: HTMLVideoElement,
  chain: Chain<Point2> | null,
): void => {
  if (!canvas) {
    return;
  }
  const width = video.videoWidth;
  const height = video.videoHeight;
  if (width === 0 || height === 0) {
    return;
  }
  if (canvas.width !== width) {
    canvas.width = width;
  }
  if (canvas.height !== height) {
    canvas.height = height;
  }

  const context = canvas.getContext("2d");
  if (!context) {
    return;
  }
  context.clearRect(0, 0, width, height);
  if (!chain) {
    return;
  }

  const points = [chain.shoulder, chain.elbow, chain.wrist].map((joint) => ({
    x: joint.x * width,
    y: joint.y * height,
  }));
  const [shoulder, elbow, wrist] = points;
  if (!shoulder || !elbow || !wrist) {
    return;
  }

  context.lineWidth = 5;
  context.lineCap = "round";
  context.lineJoin = "round";
  context.strokeStyle = "rgba(167, 139, 250, 0.9)";
  context.shadowColor = "rgba(139, 92, 246, 0.9)";
  context.shadowBlur = 12;
  context.beginPath();
  context.moveTo(shoulder.x, shoulder.y);
  context.lineTo(elbow.x, elbow.y);
  context.lineTo(wrist.x, wrist.y);
  context.stroke();

  context.fillStyle = "#f5f3ff";
  for (const point of points) {
    context.beginPath();
    context.arc(point.x, point.y, 4.5, 0, Math.PI * 2);
    context.fill();
  }
};

type PoseState =
  | { status: "idle" | "loading" }
  | { status: "tracking"; posed: boolean }
  | { status: "error"; error: string };

export type PoseStatus = PoseState["status"];

interface UsePoseCurlsOptions {
  /** The camera only spins up when the card is on screen — this is the switch. */
  enabled: boolean;
  /** Fired once per completed curl of the tracked arm. */
  onRep: () => void;
}

interface UsePoseCurls {
  videoRef: React.RefObject<HTMLVideoElement | null>;
  canvasRef: React.RefObject<HTMLCanvasElement | null>;
  status: PoseStatus;
  /** Set alongside status "error"; already human-readable. */
  error: string | null;
  /** True once a full arm is actually in frame — "the camera is on" and "I can
   * see you" are different promises. */
  posed: boolean;
  /** Re-run startup after a denial or a failed model fetch. */
  retry: () => void;
}

export const usePoseCurls = ({ enabled, onRep }: UsePoseCurlsOptions): UsePoseCurls => {
  const videoRef = useRef<HTMLVideoElement>(null);
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const [attempt, setAttempt] = useState(0);
  const request = enabled ? attempt : null;
  const [activeRequest, setActiveRequest] = useState(request);
  const [pose, setPose] = useState<PoseState>({ status: enabled ? "loading" : "idle" });

  if (activeRequest !== request) {
    setActiveRequest(request);
    setPose({ status: request === null ? "idle" : "loading" });
  }

  // The detection loop outlives every render it starts in, so it reads the rep
  // handler through a ref rather than closing over a stale one.
  const onRepRef = useRef(onRep);
  useEffect(() => {
    onRepRef.current = onRep;
  }, [onRep]);

  const retry = useCallback(() => setAttempt((count) => count + 1), []);

  useEffect(() => {
    if (request === null) {
      return;
    }

    let cancelled = false;
    let frame = 0;
    let stream: MediaStream | null = null;
    let landmarker: PoseLandmarker | null = null;
    /** Latch: true while the tracked wrist is up and its rep already scored. */
    let flexed = false;
    /** The furthest the wrist has got since the latch last flipped — its low
     * while the arm is armed, its high while it's up. The lift is measured from
     * here, which is what keeps the count free of absolute heights. */
    let extreme = 0;
    /** The arm currently being watched, and its running average. */
    let tracked: Arm | null = null;
    let averaged: ArmPose | null = null;
    /** The other arm's bid to become the subject, and how long it's been making it. */
    let challenger: Arm | null = null;
    let challengeFrames = 0;
    let lostFrames = 0;
    /** Mirrors `posed` so the loop can skip the setState entirely on the ~59
     * frames a second where the answer hasn't changed. */
    let reported = false;

    const report = (value: boolean) => {
      if (value === reported) {
        return;
      }
      reported = value;
      setPose({ posed: value, status: "tracking" });
    };

    const fail = (cause: unknown) => {
      if (cancelled) {
        return;
      }
      cancelAnimationFrame(frame);
      landmarker?.close();
      landmarker = null;
      if (stream) {
        for (const track of stream.getTracks()) {
          track.stop();
        }
      }
      setPose({ error: cause instanceof Error ? cause.message : String(cause), status: "error" });
    };

    // Whichever arm we're on stays the subject unless the other one is
    // decisively clearer for long enough to have earned the hand-off.
    const pickSubject = (image: NormalizedLandmark[] | undefined): ArmPose | null => {
      const { held, clearest } = readArms(image, tracked);
      const contender = pickContender(clearest, held, tracked);

      if (contender && contender.arm === challenger) {
        challengeFrames += 1;
      } else {
        challengeFrames = contender ? 1 : 0;
      }
      challenger = contender ? contender.arm : null;

      return contender && (!held || challengeFrames >= ARM_SWITCH_FRAMES) ? contender : held;
    };

    const lose = (element: HTMLVideoElement) => {
      lostFrames += 1;
      if (lostFrames >= POSE_LOST_FRAMES) {
        report(false);
        tracked = null;
        averaged = null;
      }
      drawArm(canvasRef.current, element, null);
    };

    const score = (lift: number, changed: boolean) => {
      // Switching arms mid-set starts the new wrist's trip from where it is
      // rather than scoring on the hand-off: dropping a raised left arm and
      // hanging an extended right one is a change of subject, not a rep.
      if (changed) {
        flexed = false;
        extreme = lift;
        return;
      }

      if (flexed) {
        extreme = Math.max(extreme, lift);
        if (extreme - lift >= REP_LIFT) {
          flexed = false;
          extreme = lift;
        }
        return;
      }

      extreme = Math.min(extreme, lift);
      if (lift - extreme >= REP_LIFT) {
        flexed = true;
        extreme = lift;
        onRepRef.current();
      }
    };

    const boot = async () => {
      const { FilesetResolver, PoseLandmarker } = await loadVision();
      if (cancelled) {
        return;
      }

      stream = await navigator.mediaDevices.getUserMedia({
        audio: false,
        video: { facingMode: "user", height: 480, width: 640 },
      });
      if (cancelled) {
        for (const track of stream.getTracks()) {
          track.stop();
        }
        return;
      }

      const video = videoRef.current;
      if (!video) {
        for (const track of stream.getTracks()) {
          track.stop();
        }
        return;
      }
      video.srcObject = stream;
      await video.play();
      if (cancelled) {
        return;
      }

      const vision = await FilesetResolver.forVisionTasks(WASM_BASE_URL);
      if (cancelled) {
        return;
      }
      landmarker = await PoseLandmarker.createFromOptions(vision, {
        baseOptions: { delegate: "GPU", modelAssetPath: MODEL_URL },
        numPoses: 1,
        runningMode: "VIDEO",
      });
      if (cancelled) {
        landmarker.close();
        return;
      }

      setPose({ posed: false, status: "tracking" });

      let lastVideoTime = -1;
      let lastTimestamp = 0;

      const detect = () => {
        frame = requestAnimationFrame(detect);
        const element = videoRef.current;
        const model = landmarker;
        if (!element || !model || element.readyState < 2) {
          return;
        }
        if (element.currentTime === lastVideoTime) {
          return;
        }
        lastVideoTime = element.currentTime;

        // MediaPipe rejects a timestamp that doesn't advance.
        const now = performance.now();
        const timestamp = now > lastTimestamp ? now : lastTimestamp + 1;
        lastTimestamp = timestamp;

        const result = model.detectForVideo(element, timestamp);
        const [image] = result.landmarks;

        const subject = pickSubject(image);
        if (!subject) {
          lose(element);
          return;
        }

        lostFrames = 0;
        report(true);

        const changed = subject.arm !== tracked;
        if (changed) {
          tracked = subject.arm;
          averaged = null;
          challenger = null;
          challengeFrames = 0;
        }

        const smoothed = averaged ? blendPose(averaged, subject) : subject;
        averaged = smoothed;
        drawArm(canvasRef.current, element, smoothed.image);

        const aspect = (element.videoWidth || 1) / (element.videoHeight || 1);
        const lift = wristLift(smoothed.image, aspect);
        if (lift === null) {
          return;
        }
        score(lift, changed);
      };

      detect();
    };

    const start = async () => {
      try {
        await boot();
      } catch (error) {
        fail(error);
      }
    };
    void start();

    return () => {
      cancelled = true;
      cancelAnimationFrame(frame);
      landmarker?.close();
      if (stream) {
        for (const track of stream.getTracks()) {
          track.stop();
        }
      }
    };
  }, [request]);

  return {
    canvasRef,
    error: pose.status === "error" ? pose.error : null,
    posed: pose.status === "tracking" && pose.posed,
    retry,
    status: pose.status,
    videoRef,
  };
};
```
