Section titles decode as they enter, like a split-flap board. Character metrics are locked so the scramble never reflows the layout.
The heading is split into per-character spans. A timer steps through a fixed number of rolls; at each step, characters left of a moving reveal index show their real glyph and the rest show a random capital letter. Character widths and the container size are measured and locked before the first roll, so the scramble never reflows the layout. A synthetic click replays the shuffle from the loop timer.
const totalSteps = rolls + 1;
const stepDuration = (duration * 1000) / totalSteps;
const step = () => {
if (currentStep >= totalSteps) {
setDisplayChars(originalChars.map(toChar)); // settle on the real text
return;
}
const progress = currentStep / (totalSteps - 1);
setDisplayChars(originalChars.map((char, i) => {
if (char === " ") return space(i);
const revealed = i / originalChars.length < progress;
if (revealed) return { id: i, original: char, current: char };
const random = scrambleCharset[Math.floor(Math.random() * scrambleCharset.length)];
return { id: i, original: char, current: random };
}));
currentStep++;
timersRef.current.push(setTimeout(step, stepDuration));
};