@gfazioli/mantine-text-animate
Version:
The TextAnimate component allows you to animate text with various effects.
259 lines (255 loc) • 7.7 kB
JavaScript
'use client';
;
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