Files
CityCards-Website/src/components/HandwrittenText.tsx
priyanshuvish 97969c079b new src added
2025-10-09 19:03:24 +05:30

227 lines
6.2 KiB
TypeScript

import { useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'motion/react';
interface HandwrittenTextProps {
text: string;
className?: string;
style?: React.CSSProperties;
speed?: number; // Characters per second
startDelay?: number; // Delay before animation starts (ms)
onComplete?: () => void;
autoStart?: boolean;
}
export function HandwrittenText({
text,
className = "",
style = {},
speed = 8,
startDelay = 0,
onComplete,
autoStart = true
}: HandwrittenTextProps) {
const [displayedText, setDisplayedText] = useState('');
const [currentIndex, setCurrentIndex] = useState(0);
const [isWriting, setIsWriting] = useState(false);
const [showCursor, setShowCursor] = useState(false);
// Split text into characters, preserving line breaks
const characters = text.split('');
useEffect(() => {
if (!autoStart) return;
const startTimer = setTimeout(() => {
setIsWriting(true);
setShowCursor(true);
}, startDelay);
return () => clearTimeout(startTimer);
}, [autoStart, startDelay]);
useEffect(() => {
if (!isWriting || currentIndex >= characters.length) {
if (currentIndex >= characters.length) {
// Hide cursor after a delay
const cursorTimer = setTimeout(() => {
setShowCursor(false);
setIsWriting(false);
onComplete?.();
}, 800);
return () => clearTimeout(cursorTimer);
}
return;
}
const char = characters[currentIndex];
const baseDelay = 1000 / speed;
// Variable delays for more natural writing
let delay = baseDelay;
if (char === ' ') {
delay = baseDelay * 0.5; // Spaces are quicker
} else if (char === '\n') {
delay = baseDelay * 2; // Line breaks take longer
} else if (['.', '!', '?'].includes(char)) {
delay = baseDelay * 1.5; // Punctuation takes a bit longer
} else if ([',', ';', ':'].includes(char)) {
delay = baseDelay * 1.2;
} else {
// Add some randomness to letter timing
delay = baseDelay * (0.8 + Math.random() * 0.4);
}
const timer = setTimeout(() => {
setDisplayedText(prev => prev + char);
setCurrentIndex(prev => prev + 1);
}, delay);
return () => clearTimeout(timer);
}, [isWriting, currentIndex, characters, speed, onComplete]);
// Reset function for external control
const reset = () => {
setDisplayedText('');
setCurrentIndex(0);
setIsWriting(false);
setShowCursor(false);
};
const start = () => {
reset();
setTimeout(() => {
setIsWriting(true);
setShowCursor(true);
}, startDelay);
};
// Expose control methods
useEffect(() => {
if (typeof window !== 'undefined') {
(window as any).handwrittenTextControls = { reset, start };
}
}, []);
return (
<div className={`relative w-fit h-fit inline-block ${className}`} style={style}>
{/* Main text with character-by-character animation */}
<motion.div
className="relative"
style={{ whiteSpace: 'pre-line' }}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
>
{displayedText.split('').map((char, index) => (
<motion.span
key={`${index}-${char}`}
initial={{
opacity: 0,
scale: 0.8,
y: 2
}}
animate={{
opacity: 1,
scale: 1,
y: 0
}}
transition={{
duration: 0.2,
ease: "easeOut",
delay: 0
}}
style={{
display: char === '\n' ? 'block' : 'inline',
width: char === '\n' ? '100%' : 'auto',
height: char === '\n' ? '0' : 'auto'
}}
>
{char === '\n' ? '' : char}
</motion.span>
))}
{/* Writing cursor/pen effect */}
<AnimatePresence>
{showCursor && (
<motion.span
className="inline-block"
initial={{ opacity: 0, scale: 0 }}
animate={{
opacity: [0.3, 1, 0.3],
scale: [0.8, 1.1, 0.8],
rotate: [0, 2, -2, 0]
}}
exit={{ opacity: 0, scale: 0 }}
transition={{
opacity: {
duration: 0.8,
repeat: Infinity,
ease: "easeInOut"
},
scale: {
duration: 0.8,
repeat: Infinity,
ease: "easeInOut"
},
rotate: {
duration: 1.2,
repeat: Infinity,
ease: "easeInOut"
}
}}
style={{
color: 'rgba(85, 70, 50, 0.6)',
marginLeft: '1px'
}}
>
|
</motion.span>
)}
</AnimatePresence>
</motion.div>
{/* Ink flow effect */}
<motion.div
className="absolute inset-0 pointer-events-none"
style={{
background: `
radial-gradient(ellipse at 20% 30%, rgba(85, 70, 50, 0.05) 0%, transparent 60%),
radial-gradient(ellipse at 80% 70%, rgba(101, 84, 63, 0.04) 0%, transparent 50%)
`,
filter: 'blur(2px)',
borderRadius: '4px'
}}
initial={{ opacity: 0 }}
animate={{ opacity: isWriting ? 0.3 : 0 }}
transition={{ duration: 0.5 }}
/>
</div>
);
}
// Hook for controlling the animation externally
export function useHandwrittenText(autoStart = true) {
const [isComplete, setIsComplete] = useState(false);
const reset = () => {
setIsComplete(false);
if (typeof window !== 'undefined' && (window as any).handwrittenTextControls) {
(window as any).handwrittenTextControls.reset();
}
};
const start = () => {
setIsComplete(false);
if (typeof window !== 'undefined' && (window as any).handwrittenTextControls) {
(window as any).handwrittenTextControls.start();
}
};
return {
isComplete,
reset,
start,
onComplete: () => setIsComplete(true)
};
}