11"use client" ;
22
3- import { useEffect , useState } from "react" ;
4-
53import { cn } from "@/lib/utils" ;
4+ import { motion , MotionProps } from "motion/react" ;
5+ import { useEffect , useRef , useState } from "react" ;
66
7- interface TypingAnimationProps {
8- text : string ;
9- duration ?: number ;
7+ interface TypingAnimationProps extends MotionProps {
8+ children : string ;
109 className ?: string ;
10+ duration ?: number ;
11+ delay ?: number ;
12+ as ?: React . ElementType ;
13+ startOnView ?: boolean ;
1114}
1215
1316export default function TypingAnimation ( {
14- text,
15- duration = 200 ,
17+ children,
1618 className,
19+ duration = 100 ,
20+ delay = 0 ,
21+ as : Component = "div" ,
22+ startOnView = false ,
23+ ...props
1724} : TypingAnimationProps ) {
25+ const MotionComponent = motion . create ( Component , {
26+ forwardMotionProps : true ,
27+ } ) ;
28+
1829 const [ displayedText , setDisplayedText ] = useState < string > ( "" ) ;
19- const [ i , setI ] = useState < number > ( 0 ) ;
30+ const [ started , setStarted ] = useState ( false ) ;
31+ const elementRef = useRef < HTMLElement | null > ( null ) ;
32+
33+ useEffect ( ( ) => {
34+ if ( ! startOnView ) {
35+ const startTimeout = setTimeout ( ( ) => {
36+ setStarted ( true ) ;
37+ } , delay ) ;
38+ return ( ) => clearTimeout ( startTimeout ) ;
39+ }
40+
41+ const observer = new IntersectionObserver (
42+ ( [ entry ] ) => {
43+ if ( entry . isIntersecting ) {
44+ setTimeout ( ( ) => {
45+ setStarted ( true ) ;
46+ } , delay ) ;
47+ observer . disconnect ( ) ;
48+ }
49+ } ,
50+ { threshold : 0.1 }
51+ ) ;
52+
53+ if ( elementRef . current ) {
54+ observer . observe ( elementRef . current ) ;
55+ }
56+
57+ return ( ) => observer . disconnect ( ) ;
58+ } , [ delay , startOnView ] ) ;
2059
2160 useEffect ( ( ) => {
61+ if ( ! started ) return ;
62+
63+ let i = 0 ;
2264 const typingEffect = setInterval ( ( ) => {
23- if ( i < text . length ) {
24- setDisplayedText ( text . substring ( 0 , i + 1 ) ) ;
25- setI ( i + 1 ) ;
65+ if ( i < children . length ) {
66+ setDisplayedText ( children . substring ( 0 , i + 1 ) ) ;
67+ i ++ ;
2668 } else {
2769 clearInterval ( typingEffect ) ;
2870 }
@@ -31,16 +73,18 @@ export default function TypingAnimation({
3173 return ( ) => {
3274 clearInterval ( typingEffect ) ;
3375 } ;
34- } , [ duration , i ] ) ;
76+ } , [ children , duration , started ] ) ;
3577
3678 return (
37- < h1
79+ < MotionComponent
80+ ref = { elementRef }
3881 className = { cn (
39- "font-display text-center text- 4xl font-bold leading-[5rem] tracking-[-0.02em] drop-shadow-sm " ,
40- className ,
82+ "text-4xl font-bold leading-[5rem] tracking-[-0.02em]" ,
83+ className
4184 ) }
85+ { ...props }
4286 >
43- { displayedText ? displayedText : text }
44- </ h1 >
87+ { displayedText }
88+ </ MotionComponent >
4589 ) ;
4690}
0 commit comments