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
JavaScript
"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))) }) }));
}