84 lines
1.9 KiB
TypeScript
84 lines
1.9 KiB
TypeScript
"use client";
|
|
|
|
import { cn } from "@/lib/utils";
|
|
import { motion, MotionProps } from "framer-motion";
|
|
import { useEffect, useRef, useState } from "react";
|
|
|
|
interface TypingAnimationProps extends MotionProps {
|
|
children: string;
|
|
className?: string;
|
|
duration?: number;
|
|
delay?: number;
|
|
as?: React.ElementType;
|
|
startOnView?: boolean;
|
|
}
|
|
|
|
export function TypingAnimation({
|
|
children,
|
|
className,
|
|
duration = 50,
|
|
delay = 0,
|
|
as: Component = "div",
|
|
startOnView = true,
|
|
...props
|
|
}: TypingAnimationProps) {
|
|
const MotionComponent = motion.create(Component, {
|
|
forwardMotionProps: true,
|
|
});
|
|
|
|
const [displayedText, setDisplayedText] = useState<string>("");
|
|
const [started, setStarted] = useState(false);
|
|
const elementRef = useRef<HTMLElement | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (!startOnView) {
|
|
const startTimeout = setTimeout(() => {
|
|
setStarted(true);
|
|
}, delay);
|
|
return () => clearTimeout(startTimeout);
|
|
}
|
|
|
|
const observer = new IntersectionObserver(
|
|
([entry]) => {
|
|
if (entry.isIntersecting) {
|
|
setTimeout(() => {
|
|
setStarted(true);
|
|
}, delay);
|
|
observer.disconnect();
|
|
}
|
|
},
|
|
{ threshold: 0.1 }
|
|
);
|
|
|
|
if (elementRef.current) {
|
|
observer.observe(elementRef.current);
|
|
}
|
|
|
|
return () => observer.disconnect();
|
|
}, [delay, startOnView]);
|
|
|
|
useEffect(() => {
|
|
if (!started) return;
|
|
|
|
let i = 0;
|
|
const typingEffect = setInterval(() => {
|
|
if (i < children.length) {
|
|
setDisplayedText(children.substring(0, i + 1));
|
|
i++;
|
|
} else {
|
|
clearInterval(typingEffect);
|
|
}
|
|
}, duration);
|
|
|
|
return () => {
|
|
clearInterval(typingEffect);
|
|
};
|
|
}, [children, duration, started]);
|
|
|
|
return (
|
|
<MotionComponent ref={elementRef} className={cn("", className)} {...props}>
|
|
{displayedText}
|
|
</MotionComponent>
|
|
);
|
|
}
|