ComponentsMr Robot–style. requestAnimationFrame + progress 0→1, settled = floor(progress * length). Re-trigger on hover.
TextScrambleClient
Esempio01
hyper-text.tsx 001Default · scrambles on mount002Hero · big mono display003Brand · logotype tsxsrc/components/hyper-text.tsx
"use client";
import { useEffect, useRef, useState } from "react";
import { cn } from "@/lib/utils";
const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
export type HyperTextProps = {
children: string;
/** Total scramble duration in ms. Default 800. */
duration?: number;
/** Wrapper element. Default `"p"`. */
as?: "h1" | "h2" | "h3" | "p" | "span";
/** Trigger scramble when the user hovers. Default true. */
animateOnHover?: boolean;
className?: string;
};
/**
* <HyperText/> — characters flicker through random uppercase glyphs before
* settling left-to-right on the final word. Mr Robot–style scramble.
*/
export function HyperText({
children,
duration = 800,
as: Tag = "p",
animateOnHover = true,
className,
}: HyperTextProps) {
const target = children;
const [display, setDisplay] = useState(() => target.split(""));
const rafRef = useRef<number | null>(null);
const startRef = useRef<number>(0);
const run = () => {
if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
startRef.current = performance.now();
const tick = (now: number) => {
const elapsed = now - startRef.current;
const progress = Math.min(1, elapsed / duration);
const settled = Math.floor(progress * target.length);
const next = target.split("").map((ch, idx) => {
if (idx < settled || ch === " ") return ch;
return ALPHABET[Math.floor(Math.random() * ALPHABET.length)];
});
setDisplay(next);
if (progress < 1) {
rafRef.current = requestAnimationFrame(tick);
} else {
setDisplay(target.split(""));
rafRef.current = null;
}
};
rafRef.current = requestAnimationFrame(tick);
};
useEffect(() => {
run();
return () => {
if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [target, duration]);
return (
<Tag
className={cn("inline-flex font-mono", className)}
onPointerEnter={animateOnHover ? run : undefined}
>
{display.map((ch, i) => (
<span key={i} className="inline-block">
{ch === " " ? " " : ch}
</span>
))}
</Tag>
);
}
Note — Glifi presi da [A-Z]. Spazi non vengono scrambled. Re-runna ad ogni pointerEnter se animateOnHover (default true).
Prompt LLM02
Incolla in Claude o ChatGPT per generare la tua variante. Include il contesto del brand, i token e i vincoli del progetto.
Sei un senior frontend engineer. Stai lavorando su un sito Next.js 16 + React 19 + Tailwind v4 in italiano, look chanhdai-inspired: colonna stretta 672px, Geist Sans + Geist Mono, hairline 1px, divisori a stripe diagonale, palette zinc.
Token CSS disponibili: --bg, --bg-alt, --fg, --fg-muted, --fg-soft, --border, --border-strong, --accent. Usa SEMPRE queste variabili tramite le utility tailwind generate (bg-bg, text-fg-muted, border-border, ecc.). Helper "cn" da "@/lib/utils". Niente librerie UI extra: solo lucide-react e tailwind-merge.
Genera un componente <HyperText> Mr Robot–style scramble.
Props:
- children: string.
- duration?: number (ms totali, default 800).
- as?: "h1"|"h2"|"h3"|"p"|"span" (default "p").
- animateOnHover?: boolean (default true).
- className?: string.
Implementazione:
- "use client". useState<string[]> per il display, useRef<number | null> per l'rAF id.
- run(): cancelAnimationFrame, performance.now() come start, tick(now) calcola progress 0→1, settled = floor(progress * length). Per ogni char: se i < settled o ch === " " → final, altrimenti random uppercase.
- Trigger su mount + onPointerEnter se animateOnHover.
Output: file completo src/components/hyper-text.tsx.
Uso tipico03
<HyperText animateOnHover duration={800}>HELLO WORLD</HyperText>
Dipendenze04
Ti è servito? Dimmelo, oppure proponi il prossimo componente.