mirror of
https://github.com/Since-3/since3-website.git
synced 2026-10-12 19:51:16 +00:00
62 lines
1.6 KiB
TypeScript
62 lines
1.6 KiB
TypeScript
"use client"
|
|
|
|
import { useRef } from "react"
|
|
|
|
import { cn } from "@/lib/utils"
|
|
|
|
const GLYPHS = "!<>-_\\/[]{}=+*^?#01"
|
|
const DURATION_MS = 420
|
|
|
|
/** Link whose label decodes itself on hover, like terminal output. */
|
|
export function ScrambleLink({
|
|
href,
|
|
children,
|
|
className,
|
|
onClick,
|
|
}: {
|
|
href: string
|
|
children: string
|
|
className?: string
|
|
onClick?: () => void
|
|
}) {
|
|
const textRef = useRef<HTMLSpanElement>(null)
|
|
const frameRef = useRef(0)
|
|
|
|
const scramble = () => {
|
|
const el = textRef.current
|
|
if (!el || window.matchMedia("(prefers-reduced-motion: reduce)").matches) return
|
|
cancelAnimationFrame(frameRef.current)
|
|
const start = performance.now()
|
|
|
|
const tick = (now: number) => {
|
|
const progress = Math.min((now - start) / DURATION_MS, 1)
|
|
const revealed = Math.floor(progress * children.length)
|
|
el.textContent = children
|
|
.split("")
|
|
.map((char, index) =>
|
|
index < revealed || char === " "
|
|
? char
|
|
: GLYPHS[Math.floor(Math.random() * GLYPHS.length)]
|
|
)
|
|
.join("")
|
|
if (progress < 1) frameRef.current = requestAnimationFrame(tick)
|
|
}
|
|
frameRef.current = requestAnimationFrame(tick)
|
|
}
|
|
|
|
return (
|
|
<a
|
|
href={href}
|
|
onPointerEnter={scramble}
|
|
onFocus={scramble}
|
|
onClick={onClick}
|
|
aria-label={children}
|
|
className={cn("inline-block", className)}
|
|
>
|
|
{/* Fixed width per character so the scramble never shifts the layout */}
|
|
<span ref={textRef} aria-hidden="true" className="font-mono">
|
|
{children}
|
|
</span>
|
|
</a>
|
|
)
|
|
}
|