All components
Hyper Text
textA text animation that scrambles letters before revealing the final text.
responsive · 340px
Install
Same command in any shadcn project — React (Vite/CRA), Next.js, Remix, Astro, and more:
$
npx shadcn@latest add https://your-domain/r/hyper-text.jsonUsage
"use client";
import { HyperText } from "@/registry/magic-ui/hyper-text";
export default function Demo() {
return (
<div className="flex items-center justify-center w-full max-w-md p-8">
<HyperText>Hover over me</HyperText>
</div>
);
}Component source
"use client"
import {
useEffect,
useRef,
useState,
type ComponentType,
type RefAttributes,
} from "react"
import {
AnimatePresence,
motion,
type DOMMotionComponents,
type HTMLMotionProps,
type MotionProps,
} from "motion/react"
import { cn } from "@/lib/utils"
type CharacterSet = string[] | readonly string[]
const motionElements = {
article: motion.article,
div: motion.div,
h1: motion.h1,
h2: motion.h2,
h3: motion.h3,
h4: motion.h4,
h5: motion.h5,
h6: motion.h6,
li: motion.li,
p: motion.p,
section: motion.section,
span: motion.span,
} as const
type MotionElementType = Extract<
keyof DOMMotionComponents,
keyof typeof motionElements
>
type HyperTextMotionComponent = ComponentType<
Omit<HTMLMotionProps<"div">, "ref"> & RefAttributes<HTMLElement>
>
interface HyperTextProps extends Omit<MotionProps, "children"> {
/** The text content to be animated */
children: string
/** Optional className for styling */
className?: string
/** Duration of the animation in milliseconds */
duration?: number
/** Delay before animation starts in milliseconds */
delay?: number
/** Component to render as - defaults to div */
as?: MotionElementType
/** Whether to start animation when element comes into view */
startOnView?: boolean
/** Whether to trigger animation on hover */
animateOnHover?: boolean
/** Custom character set for scramble effect. Defaults to uppercase alphabet */
characterSet?: CharacterSet
}
const DEFAULT_CHARACTER_SET = Object.freeze(
"ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("")
) as readonly string[]
const getRandomInt = (max: number): number => Math.floor(Math.random() * max)
export function HyperText({
children,
className,
duration = 800,
delay = 0,
as: Component = "div",
startOnView = false,
animateOnHover = true,
characterSet = DEFAULT_CHARACTER_SET,
...props
}: HyperTextProps) {
const MotionComponent = motionElements[Component] as HyperTextMotionComponent
const [displayText, setDisplayText] = useState<string[]>(() =>
children.split("")
)
const [isAnimating, setIsAnimating] = useState(false)
const iterationCount = useRef(0)
const elementRef = useRef<HTMLElement | null>(null)
const handleAnimationTrigger = () => {
if (animateOnHover && !isAnimating) {
iterationCount.current = 0
setIsAnimating(true)
}
}
// Handle animation start based on view or delay
useEffect(() => {
if (!startOnView) {
const startTimeout = setTimeout(() => {
setIsAnimating(true)
}, delay)
return () => clearTimeout(startTimeout)
}
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setTimeout(() => {
setIsAnimating(true)
}, delay)
observer.disconnect()
}
},
{ threshold: 0.1, rootMargin: "-30% 0px -30% 0px" }
)
if (elementRef.current) {
observer.observe(elementRef.current)
}
return () => observer.disconnect()
}, [delay, startOnView])
// Handle scramble animation
useEffect(() => {
let animationFrameId: number | null = null
if (isAnimating) {
const maxIterations = children.length
const startTime = performance.now()
const animate = (currentTime: number) => {
const elapsed = currentTime - startTime
const progress = Math.min(elapsed / duration, 1)
iterationCount.current = progress * maxIterations
setDisplayText((currentText) =>
currentText.map((letter, index) =>
letter === " "
? letter
: index <= iterationCount.current
? children[index]
: characterSet[getRandomInt(characterSet.length)]
)
)
if (progress < 1) {
animationFrameId = requestAnimationFrame(animate)
} else {
setIsAnimating(false)
}
}
animationFrameId = requestAnimationFrame(animate)
}
return () => {
if (animationFrameId !== null) {
cancelAnimationFrame(animationFrameId)
}
}
}, [children, duration, isAnimating, characterSet])
return (
<MotionComponent
ref={elementRef}
className={cn("overflow-hidden py-2 text-4xl font-bold", className)}
onMouseEnter={handleAnimationTrigger}
{...props}
>
<AnimatePresence>
{displayText.map((letter, index) => (
<motion.span
key={index}
className={cn("font-mono", letter === " " ? "w-3" : "")}
>
{letter.toUpperCase()}
</motion.span>
))}
</AnimatePresence>
</MotionComponent>
)
}Dependencies
motion
Source: Magic UI