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.

Step 1 of 4: Naive. 177 calls, O(2^n), space O(n).
Refactor: recursive fib → O(1) space (n = 10)
fib.tsStep 1 / 4Naive
function fib(n) {
if (n <= 1) return n
return fib(n - 1) + fib(n - 2)
}
calls177O(2^n)spaceO(n)

Each call branches into two. Subproblems are recomputed exponentially.

1 / 4

Installation

npx shadcn@latest add https://craftbits.dev/r/dp-fib-ratchet.json

Usage

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

  1. 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.
  2. Animated swap. The code block, ops counter, complexity badge, and space badge each live inside AnimatePresence with mode="wait" and initial={false}, so swaps are smooth between steps but the first render is instant. Reduced-motion users always snap.
  3. Controlled + uncontrolled. currentStep + onStepChange is the canonical Radix controlled pattern. defaultStep lets the component own the step internally. The step is clamped to steps.length - 1, so passing an out-of-range value never crashes.
  4. 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.
  5. Built-in chrome is optional. hideControls strips the prev / next buttons; hideCaption strips the description line; hideOpsMeter strips 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.
  6. 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.
  7. Empty steps array. Renders a muted "no steps" placeholder instead of crashing.

Props

PropTypeDefaultDescription
stepsreadonly DPFibRatchetStep[]canonical 4-step ratchetRefactor steps to play. Each step holds a label, code snapshot, ops count, time complexity, optional space complexity, optional ops unit, and optional caption.
currentStepnumberControlled active step index. Pair with onStepChange. Clamped to [0, steps.length - 1].
defaultStepnumber0Uncontrolled initial step.
onStepChange(next: number) => voidFires 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.
titleReactNodeOptional title rendered above the code block.
prevLabelstring"Prev"Label for the previous-step button.
nextLabelstring"Next"Label for the next-step button.
hideControlsbooleanfalseHide the built-in prev / next chrome.
hideOpsMeterbooleanfalseHide the ops counter, time complexity badge, and space complexity badge.
hideCaptionbooleanfalseHide the per-step caption line.
transitionTransitionSPRINGS.smoothOverride code / meter / caption transitions. Reduced-motion users snap regardless.
classNamestringMerged onto the outer <div> via cn().

Accessibility

  • The root carries aria-roledescription="dp fibonacci refactor ratchet" and an off-screen aria-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-label naming 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 and aria-label naming the absolute step number and label.
  • Prev / next buttons carry aria-label and respect disabled at 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 by MagicMoveBlock, a breakthrough celebration with SFX and starburst, an amber structural-insight card, and a final three-tile timeline strip narrated by RichText. 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 the title, hideControls, hideOpsMeter, and hideCaption slots.