UNPKG

@gfazioli/mantine-text-animate

Version:

The TextAnimate component allows you to animate text with various effects.

259 lines (255 loc) 7.7 kB
'use client'; 'use strict'; var React = require('react'); function createInitialCharacters(length) { return Array.from({ length }, () => ({ current: " ", next: " ", isFlipping: false, settled: false, flipKey: 0 })); } function useSplitFlap({ value, animate = true, speed: _speed = 1, characterSet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 ", flipDuration = 300, staggerDelay = 80, delay = 0, onCompleted }) { const speed = Math.max(0.1, _speed); const targetText = value.toUpperCase(); const [characters, setCharacters] = React.useState( () => createInitialCharacters(targetText.length) ); const [isAnimating, setIsAnimating] = React.useState(false); const timeoutsRef = React.useRef([]); const delayTimeoutRef = React.useRef(null); const isAnimatingRef = React.useRef(false); const animatePropRef = React.useRef(animate); const animationCompletedRef = React.useRef(false); const onCompletedRef = React.useRef(onCompleted); React.useEffect(() => { onCompletedRef.current = onCompleted; }, [onCompleted]); const clearAllTimeouts = React.useCallback(() => { timeoutsRef.current.forEach(clearTimeout); timeoutsRef.current = []; if (delayTimeoutRef.current) { clearTimeout(delayTimeoutRef.current); delayTimeoutRef.current = null; } }, []); const animateCharacter = React.useCallback( (index, target) => { const effectiveFlipDuration = flipDuration / speed; const spaceIndex = characterSet.indexOf(" "); const targetIndex = characterSet.indexOf(target); let sequence = []; if (target === " ") { setCharacters((prev) => { const next = [...prev]; next[index] = { current: " ", next: " ", isFlipping: false, settled: true, flipKey: 0 }; return next; }); return; } if (targetIndex === -1) { setCharacters((prev) => { const next = [...prev]; next[index] = { current: target, next: target, isFlipping: false, settled: true, flipKey: 0 }; return next; }); return; } const startIdx = spaceIndex !== -1 ? spaceIndex : 0; if (startIdx <= targetIndex) { for (let i = startIdx; i <= targetIndex; i++) { sequence.push(characterSet[i]); } } else { for (let i = startIdx; i < characterSet.length; i++) { sequence.push(characterSet[i]); } for (let i = 0; i <= targetIndex; i++) { sequence.push(characterSet[i]); } } if (sequence.length > 0 && sequence[0] === " ") { sequence = sequence.slice(1); } sequence.forEach((nextChar, stepIndex) => { const timeout = setTimeout(() => { if (!isAnimatingRef.current) { return; } setCharacters((prev) => { const updated = [...prev]; const currentChar = stepIndex === 0 ? " " : sequence[stepIndex - 1]; updated[index] = { current: currentChar, next: nextChar, isFlipping: true, settled: false, flipKey: stepIndex + 1 }; return updated; }); }, stepIndex * effectiveFlipDuration); timeoutsRef.current.push(timeout); }); const settleTimeout = setTimeout(() => { if (!isAnimatingRef.current) { return; } const lastChar = sequence[sequence.length - 1]; setCharacters((prev) => { const updated = [...prev]; updated[index] = { current: lastChar, next: lastChar, isFlipping: false, settled: true, flipKey: sequence.length + 1 }; return updated; }); }, sequence.length * effectiveFlipDuration); timeoutsRef.current.push(settleTimeout); }, [characterSet, flipDuration, speed] ); const start = React.useCallback(() => { if (typeof window === "undefined") { return; } const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; if (prefersReducedMotion) { setCharacters( targetText.split("").map((char) => ({ current: char, next: char, isFlipping: false, settled: true, flipKey: 0 })) ); setIsAnimating(false); isAnimatingRef.current = false; animationCompletedRef.current = true; onCompletedRef.current?.(); return; } clearAllTimeouts(); setCharacters(createInitialCharacters(targetText.length)); setIsAnimating(true); isAnimatingRef.current = true; animationCompletedRef.current = false; const effectiveFlipDuration = flipDuration / speed; delayTimeoutRef.current = setTimeout(() => { targetText.split("").forEach((targetChar, index) => { const charStartTimeout = setTimeout(() => { if (!isAnimatingRef.current) { return; } animateCharacter(index, targetChar); }, staggerDelay * index); timeoutsRef.current.push(charStartTimeout); }); const lastCharStart = staggerDelay * (targetText.length - 1); const maxFlips = characterSet.length; const totalDuration = lastCharStart + maxFlips * effectiveFlipDuration + effectiveFlipDuration; const completionTimeout = setTimeout(() => { setCharacters( targetText.split("").map((char) => ({ current: char, next: char, isFlipping: false, settled: true, flipKey: 0 })) ); setIsAnimating(false); isAnimatingRef.current = false; animationCompletedRef.current = true; onCompletedRef.current?.(); }, totalDuration); timeoutsRef.current.push(completionTimeout); }, delay * 1e3); }, [ targetText, clearAllTimeouts, animateCharacter, flipDuration, speed, staggerDelay, delay, characterSet.length ]); const stop = React.useCallback(() => { clearAllTimeouts(); setIsAnimating(false); isAnimatingRef.current = false; }, [clearAllTimeouts]); const reset = React.useCallback(() => { clearAllTimeouts(); setCharacters(createInitialCharacters(targetText.length)); setIsAnimating(false); isAnimatingRef.current = false; animationCompletedRef.current = false; }, [clearAllTimeouts, targetText.length]); React.useEffect(() => { if (animate !== animatePropRef.current) { animatePropRef.current = animate; if (animate && !isAnimatingRef.current) { animationCompletedRef.current = false; start(); } else if (!animate && isAnimatingRef.current) { stop(); } } else if (animate && !isAnimatingRef.current && !animationCompletedRef.current) { start(); } }, [animate, start, stop]); React.useEffect(() => { if (isAnimatingRef.current) { start(); } else if (animationCompletedRef.current) { animationCompletedRef.current = false; if (animate) { start(); } } else { setCharacters(createInitialCharacters(targetText.length)); } }, [targetText]); React.useEffect(() => { return () => { clearAllTimeouts(); }; }, [clearAllTimeouts]); return { characters, start, stop, reset, isAnimating }; } exports.useSplitFlap = useSplitFlap; //# sourceMappingURL=use-split-flap.cjs.map