Scramble to Word
Installation
npx shadcn@latest add https://uibrik.com/r/scramble-to-word.jsonAdd the cn helper
Imports
cnfrom@/lib/utils, whichshadcn initcreates. Add it by hand without shadcn.Copy the source into your project
components/uibrik/scramble-to-word/scramble-to-word.tsx"use client"; import { cn } from "@/lib/utils"; import { useEffect, useState } from "react"; import { SIZE, pick, type EffectProps } from "../_text-effects/shared"; // 09 — A departures board without the flip: every cell scrambles through // random characters, then the cells lock in order, left to right, so the // new word sorts itself out of the noise. const SCRAMBLE_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; const FLAP_WORDS = ["BOARDING", "GATE 21", "ON TIME", "DEPARTED"]; const randomChar = () => SCRAMBLE_CHARS[Math.floor(Math.random() * SCRAMBLE_CHARS.length)]; export function ScrambleToWord({ play, words = FLAP_WORDS, cells: FLAP_LENGTH = 8, tick: SCRAMBLE_TICK = 40, lead: SCRAMBLE_LEAD = 6, step: SCRAMBLE_STEP = 2, }: EffectProps & { words?: string[]; /** Number of cells on the board. */ cells?: number; /** Milliseconds between scramble frames. */ tick?: number; /** Frames of pure noise before the first cell locks. */ lead?: number; /** Frames between one cell locking and the next. */ step?: number; }) { const target = pick(words, play).padEnd(FLAP_LENGTH, " "); const [shown, setShown] = useState(target); const [locked, setLocked] = useState(FLAP_LENGTH); useEffect(() => { if (!play) return; let tick = 0; const id = setInterval(() => { const settled = Math.max(0, Math.floor((tick - SCRAMBLE_LEAD) / SCRAMBLE_STEP) + 1); setLocked(settled); setShown( Array.from(target, (ch, i) => (i < settled || ch === " " ? ch : randomChar())).join(""), ); if (settled >= FLAP_LENGTH) clearInterval(id); tick++; }, SCRAMBLE_TICK); return () => clearInterval(id); // eslint-disable-next-line react-hooks/exhaustive-deps }, [play, target]); return ( // cells are 28 × 36 at 20px type (16px glyphs) and scale with it <div className={cn(SIZE, "flex gap-[0.2em] font-mono")} aria-label={target.trim()}> {Array.from(shown).map((ch, i) => ( <span key={i} aria-hidden className={cn( "flex h-[2.25em] w-[1.75em] items-center justify-center rounded-md bg-neutral-100 text-[0.8em] font-medium transition-colors duration-150 dark:bg-neutral-800", i < locked ? "text-zinc-700 dark:text-zinc-200" : "text-zinc-400 dark:text-zinc-500", )} > {ch} </span> ))} </div> ); }
Adds the files to
components/uibrik/scramble-to-word. With the @uibrik namespace set up: npx shadcn@latest add @uibrik/scramble-to-word.Props
ScrambleToWord- words
- Type string[]
- ["BOARDING","GATE 21","ON TIME","DEPARTED"]
- Cycled through, one per play.
- cells
- Type number
- 8
- Number of cells on the board.
- tick
- Type number
- 40
- Time between scramble frames.
- lead
- Type number
- 6
- Frames of noise before the first cell locks.
- step
- Type number
- 2
- Frames between cells locking.
- playrequired
- Type number
- –
- Each change plays the effect again; 0 shows its resting state.