DPFibRatchet
A step-through viewer for the refactor ratchet that converts the naive recursive Fibonacci into a memoised, then tabular, then constant-space version. The caller supplies an array of steps (or accepts the canonical four-step default); the component renders one revision at a time — code block, animated ops meter, time and space complexity badges, and per-step caption — and advances on prev / next, dot click, or arrow keys.
Pure playback primitive — it does not gate, score, or quiz. Controlled (currentStep + onStepChange) and uncontrolled (defaultStep) follow the Radix pattern. The default ratchet ships four revisions: a naive recursive fib at exponential time, a memoised recursive fib at linear time, a bottom-up tabular fib at linear time with no stack, and a rolling two-scalar fib at linear time and constant space.
function fib(n) {if (n <= 1) return nreturn fib(n - 1) + fib(n - 2)}
Each call branches into two. Subproblems are recomputed exponentially.
Installation
npx shadcn@latest add https://craftbits.dev/r/dp-fib-ratchet.jsonUsage
import { DPFibRatchet } from "@craft-bits/core";
<DPFibRatchet />Controlled — the parent owns the step index so it can scrub, link the ratchet to other panels, or persist the cursor:
const [step, setStep] = useState(0);
<DPFibRatchet
currentStep={step}
onStepChange={setStep}
/>Uncontrolled with a starting offset and the built-in chrome hidden — drive the step entirely from outside:
<DPFibRatchet
defaultStep={1}
hideControls
hideCaption
/>Custom ratchet — the caller supplies the full sequence of revisions. Useful when teaching a different DP baseline (e.g. coin change, climb stairs, longest common subsequence):
<DPFibRatchet
steps={[
{
label: "Top-down",
code: "function climb(n) { /* ... */ }",
ops: 177,
complexity: "O(2^n)",
space: "O(n)",
opsUnit: "calls",
description: "Brute force baseline.",
},
{
label: "Bottom-up",
code: "function climb(n) { /* ... */ }",
ops: 9,
complexity: "O(n)",
space: "O(1)",
opsUnit: "iterations",
description: "Two scalars, no recursion.",
},
]}
/>Understanding the component
- One revision per step. Each step is a snapshot of the code at a point in the refactor, paired with the measured operation count, the time complexity class, and an optional space complexity class. Steps are independent — there is no shared mutable state across them.
- Animated swap. The code block, ops counter, complexity badge, and space badge each live inside
AnimatePresencewithmode="wait"andinitial={false}, so swaps are smooth between steps but the first render is instant. Reduced-motion users always snap. - Controlled + uncontrolled.
currentStep+onStepChangeis the canonical Radix controlled pattern.defaultSteplets the component own the step internally. The step is clamped tosteps.length - 1, so passing an out-of-range value never crashes. - Per-step ops unit. Each step can override the meter label via
opsUnit(e.g. "calls" for recursive variants, "iterations" for bottom-up). Defaults to "ops" so a one-prop usage still reads correctly. - Built-in chrome is optional.
hideControlsstrips the prev / next buttons;hideCaptionstrips the description line;hideOpsMeterstrips the ops + complexity + space row. Pass all three and you have a pure code-revision frame the parent can wrap in any chrome it likes. - Keyboard. Arrow Left / Right advance the step. The dot row is a row of buttons — Tab into it and Space / Enter jumps to that step.
- Empty steps array. Renders a muted "no steps" placeholder instead of crashing.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
steps | readonly DPFibRatchetStep[] | canonical 4-step ratchet | Refactor steps to play. Each step holds a label, code snapshot, ops count, time complexity, optional space complexity, optional ops unit, and optional caption. |
currentStep | number | — | Controlled active step index. Pair with onStepChange. Clamped to [0, steps.length - 1]. |
defaultStep | number | 0 | Uncontrolled initial step. |
onStepChange | (next: number) => void | — | Fires whenever the active step changes (prev / next / dot click / arrow keys). |
tone | "default" | "accent" | "success" | "warning" | "error" | "accent" | Highlight palette for the complexity badge and dot row. |
title | ReactNode | — | Optional title rendered above the code block. |
prevLabel | string | "Prev" | Label for the previous-step button. |
nextLabel | string | "Next" | Label for the next-step button. |
hideControls | boolean | false | Hide the built-in prev / next chrome. |
hideOpsMeter | boolean | false | Hide the ops counter, time complexity badge, and space complexity badge. |
hideCaption | boolean | false | Hide the per-step caption line. |
transition | Transition | SPRINGS.smooth | Override code / meter / caption transitions. Reduced-motion users snap regardless. |
className | string | — | Merged onto the outer <div> via cn(). |
Accessibility
- The root carries
aria-roledescription="dp fibonacci refactor ratchet"and an off-screenaria-live="polite"summary that announces the step number, step label, ops count with its unit, time complexity class, and space complexity class on every change — so screen-reader users hear the same narration sighted users see in the code header. - The code block has an
aria-labelnaming the step number and label so screen readers can locate the active revision. - The ops + complexity cluster is
role="status"so changes are announced without stealing focus. - Every dot in the step row is a real button with
aria-current="step"on the active step andaria-labelnaming the absolute step number and label. - Prev / next buttons carry
aria-labeland respectdisabledat both ends of the ratchet. - Arrow Left / Right step the ratchet; the dot row is keyboard-reachable via Tab, then Space / Enter to jump.
- All interactive targets enforce a 44 × 44px minimum hit area (per WCAG 2.5.8 AAA) regardless of how compact the dot looks.
- Tone is never the only signal — the active step layers fill, ring, and scale changes so colour-blind users see the distinction.
- Motion respects
prefers-reduced-motion: reduce— code swap, ops meter tween, caption enter / exit, and dot scale collapse to instant.
Credits
- Extracted from:
algoflashcards(src/lessons/primitives/refactor-ratchets/DPFibRatchet.tsx). The source was a 1000+ line interactive lesson bundling a complexity prediction gate on the naive recursion, tap-to-identify code-line interactions on the recursive return and the recursive calls, three-blank-then-two-blank fill-in-the-blank dropdowns that morph the code from recursive to memoised to tabular, ops-counter cascades from 177 to 19 to 9 driven byMagicMoveBlock, a breakthrough celebration with SFX and starburst, an amber structural-insight card, and a final three-tile timeline strip narrated byRichText. The library extract keeps only the playback primitive — a sequence of code revisions plus their ops count and time and space complexity classes, advanced by a controlled step index — and lets the caller compose any acts, predictions, scoring, or sound on top via thetitle,hideControls,hideOpsMeter, andhideCaptionslots.