UNPKG

xlibs-components

Version:

Modern React component library with Aceternity, MagicUI, and ShadCN components for building beautiful web applications

71 lines (70 loc) 3.3 kB
"use client"; import { jsx as _jsx } from "react/jsx-runtime"; import { cn } from '../../lib/utils.js'; import { AnimatePresence, motion } from "motion/react"; import { useEffect, useRef, useState } from "react"; const DEFAULT_CHARACTER_SET = Object.freeze("ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("")); const getRandomInt = (max) => 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 }) { const MotionComponent = motion.create(Component, { forwardMotionProps: true, }); const [displayText, setDisplayText] = useState(() => children.split("")); const [isAnimating, setIsAnimating] = useState(false); const iterationCount = useRef(0); const elementRef = useRef(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(() => { if (!isAnimating) return; const maxIterations = children.length; const startTime = performance.now(); let animationFrameId; const animate = (currentTime) => { 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 () => cancelAnimationFrame(animationFrameId); }, [children, duration, isAnimating, characterSet]); return (_jsx(MotionComponent, { ref: elementRef, className: cn("overflow-hidden py-2 text-4xl font-bold", className), onMouseEnter: handleAnimationTrigger, ...props, children: _jsx(AnimatePresence, { children: displayText.map((letter, index) => (_jsx(motion.span, { className: cn("font-mono", letter === " " ? "w-3" : ""), children: letter.toUpperCase() }, index))) }) })); }