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.
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}
Two nested passes. Every pair compared again and again.
Installation
npx shadcn@latest add https://craftbits.dev/r/sorting-ratchet.jsonUsage
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
- 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.
- Animated swap. The code block, comparison counter, and complexity 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. - Built-in chrome is optional.
hideControlsstrips the prev / next buttons;hideCaptionstrips the description line;hideOpsMeterstrips the comparison + complexity 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 SortingRatchetStep[] | canonical 3-step ratchet | Refactor steps to play. Each step holds a label, code snapshot, comparison count, complexity class, 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 comparison counter and 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="sorting refactor ratchet"and an off-screenaria-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-labelnaming 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 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, 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 thetitle,hideControls,hideOpsMeter, andhideCaptionslots.