File size: 4,142 Bytes
50a8ea3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
import React, { useState, useCallback } from 'react';

interface FoldTextProps {
  text: string;
  className?: string;
  wordClassName?: string;
  charClassName?: string;
  stagger?: number;
  duration?: number;
  perspective?: number;
  foldDirection?: 'top' | 'bottom' | 'left' | 'right';
  hoverEffect?: boolean;
}

export const FoldText: React.FC<FoldTextProps> = ({

  text,

  className = '',

  wordClassName = '',

  charClassName = '',

  stagger = 0.035,

  duration = 0.65,

  perspective = 1000,

  foldDirection = 'top',

  hoverEffect = true,

}) => {
  const [animationCycle, setAnimationCycle] = useState(0);

  const handleMouseEnter = useCallback(() => {
    if (hoverEffect) {
      setAnimationCycle((prev) => prev + 1);
    }
  }, [hoverEffect]);

  // Split text into words while tracking cumulative character index for staggered delay
  const words = text.split(' ');
  let globalCharIndex = 0;

  return (
    <span

      className={`inline-flex flex-wrap justify-center items-center gap-x-[0.28em] select-none cursor-default ${className}`}

      style={{ perspective: `${perspective}px` }}

      aria-label={text}

      onMouseEnter={handleMouseEnter}

    >

      {words.map((word, wordIdx) => {

        const wordChars = word.split('');

        return (

          <span

            key={`word-${wordIdx}-${animationCycle}`}

            className={`inline-flex whitespace-nowrap ${wordClassName}`}

            style={{ transformStyle: 'preserve-3d' }}

          >

            {wordChars.map((char) => {

              const charIndex = globalCharIndex++;

              const delay = charIndex * stagger;



              return (

                <span

                  key={`char-${charIndex}-${animationCycle}`}

                  className={`inline-block will-change-transform ${charClassName}`}

                  style={{

                    transformStyle: 'preserve-3d',

                    transformOrigin:

                      foldDirection === 'top'

                        ? '50% 0%'

                        : foldDirection === 'bottom'

                        ? '50% 100%'

                        : foldDirection === 'left'

                        ? '0% 50%'

                        : '100% 50%',

                    animation: `foldTextIn-${foldDirection} ${duration}s cubic-bezier(0.2, 0.9, 0.3, 1) ${delay}s both`,

                  }}

                >

                  {char}

                </span>

              );

            })}

          </span>

        );

      })}



      <style>{`

        @keyframes foldTextIn-top {

          0% {

            opacity: 0;

            transform: rotateX(90deg) translateY(-24px) translateZ(15px);

            filter: blur(4px);

          }

          50% {

            opacity: 0.8;

            filter: blur(1px);

          }

          100% {

            opacity: 1;

            transform: rotateX(0deg) translateY(0px) translateZ(0px);

            filter: blur(0px);

          }

        }

        @keyframes foldTextIn-bottom {

          0% {

            opacity: 0;

            transform: rotateX(-90deg) translateY(24px) translateZ(15px);

            filter: blur(4px);

          }

          100% {

            opacity: 1;

            transform: rotateX(0deg) translateY(0px) translateZ(0px);

            filter: blur(0px);

          }

        }

        @keyframes foldTextIn-left {

          0% {

            opacity: 0;

            transform: rotateY(-90deg) translateX(-24px);

            filter: blur(4px);

          }

          100% {

            opacity: 1;

            transform: rotateY(0deg) translateX(0px);

            filter: blur(0px);

          }

        }

        @keyframes foldTextIn-right {

          0% {

            opacity: 0;

            transform: rotateY(90deg) translateX(24px);

            filter: blur(4px);

          }

          100% {

            opacity: 1;

            transform: rotateY(0deg) translateX(0px);

            filter: blur(0px);

          }

        }

      `}</style>

    </span>
  );
};