SortingRatchet

A step-through viewer for the refactor ratchet that walks a sort from bubble to merge to quick. The caller supplies an array of steps (or accepts the canonical three-step default); the component renders one revision at a time — code block, animated comparison meter, complexity badge, 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 three revisions: a nested-loop bubble sort at O(n^2), a recursive merge sort at O(n log n) that splits and weaves, and a partition-based quick sort that pivots in one pass.

Step 1 of 3: Bubble. 21 comparisons, O(n^2).
Refactor: bubble sort → merge sort → quick sort (input [38, 27, 43, 3, 9, 82, 10])
sort.tsStep 1 / 3Bubble
function sort(arr) {
for (let i = 0; i < arr.length; i++) {
for (let j = 0; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
}
}
}
return arr
}
cmp21O(n^2)

Two nested passes. Every pair compared again and again.

1 / 3

Installation

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

Usage

import { SortingRatchet } from "@craft-bits/core";
 
<SortingRatchet />

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);
 
<SortingRatchet
  currentStep={step}
  onStepChange={setStep}
/>

Uncontrolled with a starting offset and the built-in chrome hidden — drive the step entirely from outside:

<SortingRatchet
  defaultStep={1}
  hideControls
  hideCaption
/>

Custom ratchet — the caller supplies the full sequence of revisions. Useful when teaching a different baseline (e.g. selection sort, heap sort, radix sort):

<SortingRatchet
  steps={[
    {
      label: "Selection",
      code: "function sort(arr) { /* ... */ }",
      ops: 21,
      complexity: "O(n^2)",
      description: "Scan for the min, swap into place, repeat.",
    },
    {
      label: "Heap",
      code: "function sort(arr) { /* ... */ }",
      ops: 17,
      complexity: "O(n log n)",
      description: "Heapify, then pop the max into the back.",
    },
  ]}
/>

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 comparison count and the complexity class. Steps are independent — there is no shared mutable state across them.
  2. Animated swap. The code block, comparison counter, and complexity 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. Built-in chrome is optional. hideControls strips the prev / next buttons; hideCaption strips the description line; hideOpsMeter strips the comparison + complexity row. Pass all three and you have a pure code-revision frame the parent can wrap in any chrome it likes.
  5. 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.
  6. Empty steps array. Renders a muted "no steps" placeholder instead of crashing.

Props

PropTypeDefaultDescription
stepsreadonly SortingRatchetStep[]canonical 3-step ratchetRefactor steps to play. Each step holds a label, code snapshot, comparison count, complexity class, 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 comparison counter and 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="sorting refactor ratchet" and an off-screen aria-live="polite" summary that announces the step number, step label, comparison count, and 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 comparison + 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, comparison meter tween, caption enter / exit, and dot scale collapse to instant.

Credits

  • Extracted from: algoflashcards (src/lessons/primitives/refactor-ratchets/SortingRatchet.tsx). The source was a 1000-line interactive lesson bundling a complexity-prediction gate, tap-to-identify code-line interactions for the outer loop and the wrong concatenation line, two stages of inline fill-in-the-blank dropdowns with per-blank teaching feedback, code morph via MagicMoveBlock, a deliberate wrong-output reveal between the divide and merge steps, a breakthrough celebration with SFX, a final timeline strip narrating "Selection → Divide → Merge!", and a track-coloured chrome. The library extract keeps only the playback primitive — a sequence of code revisions plus their comparison count and complexity class, 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.