@gfazioli/mantine-text-animate
Version:
The TextAnimate component allows you to animate text with various effects.
233 lines (229 loc) • 8.1 kB
JavaScript
'use client';
;
var core = require('@mantine/core');
var hooks = require('@mantine/hooks');
var React = require('react');
var Gradient = require('./Gradient/Gradient.cjs');
var Highlight = require('./Highlight/Highlight.cjs');
var Morphing = require('./Morphing/Morphing.cjs');
var NumberTicker = require('./NumberTicker/NumberTicker.cjs');
var RotatingText = require('./RotatingText/RotatingText.cjs');
var Spinner = require('./Spinner/Spinner.cjs');
var SplitFlap = require('./SplitFlap/SplitFlap.cjs');
var TextTicker = require('./TextTicker/TextTicker.cjs');
var Typewriter = require('./Typewriter/Typewriter.cjs');
var TextAnimate_module = require('./TextAnimate.module.css.cjs');
const defaultProps = {
delay: 0,
duration: 0.3,
segmentDelay: 0.05,
by: "word",
animation: "fade",
animateProps: {
translateDistance: "20",
scaleAmount: 2,
blurAmount: "10"
}
};
const defaultStaggerTimings = {
text: 0.06,
word: 0.05,
character: 0.03,
line: 0.06
};
const containerStyles = {
whiteSpace: "pre-wrap",
position: "relative",
display: "block",
minHeight: "1em"
};
const varsResolver = core.createVarsResolver((_, { animateProps }) => ({
root: {
"--text-animate-translation-distance": animateProps?.translateDistance ? core.getSize(animateProps.translateDistance, "translate-distance") : "20px",
"--text-animate-blur-amount": animateProps?.blurAmount ? core.getSize(animateProps.blurAmount, "blur-amount") : "10px",
"--text-animate-scale-amount": animateProps?.scaleAmount ? animateProps.scaleAmount.toString() : "0.8"
}
}));
const TextAnimate = core.polymorphicFactory((_props) => {
const { ref, ...restProps } = _props;
const props = core.useProps("TextAnimate", defaultProps, restProps);
const {
delay,
duration,
segmentClassName,
animate,
by,
animation,
segmentDelay,
animateProps,
onAnimationStart,
onAnimationEnd,
onAnimationComplete,
trigger,
triggerOptions,
loopDelay,
classNames,
style,
styles,
unstyled,
vars,
children,
className,
...others
} = props;
const staggerTiming = segmentDelay !== void 0 ? segmentDelay : defaultStaggerTimings[by ?? "character"];
const completedCountRef = React.useRef(0);
const [isAnimating, setIsAnimating] = React.useState(false);
const [loopPhase, setLoopPhase] = React.useState("in");
const loopTimerRef = React.useRef(null);
React.useEffect(() => {
return () => {
if (loopTimerRef.current) {
clearTimeout(loopTimerRef.current);
}
};
}, []);
const inViewRef = React.useRef(null);
const [inView, setInView] = React.useState(false);
React.useEffect(() => {
if (trigger !== "inView" || !inViewRef.current || typeof IntersectionObserver === "undefined") {
return;
}
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setInView(true);
observer.disconnect();
}
},
{
threshold: triggerOptions?.threshold ?? 0.1,
rootMargin: triggerOptions?.rootMargin ?? "0px"
}
);
observer.observe(inViewRef.current);
return () => observer.disconnect();
}, [trigger, triggerOptions?.threshold, triggerOptions?.rootMargin]);
const mergedRef = hooks.useMergedRef(ref, inViewRef);
let effectiveAnimate = animate;
if (animate === "loop") {
effectiveAnimate = loopPhase;
}
if (trigger === "inView") {
effectiveAnimate = inView ? animate === "loop" ? loopPhase : animate || "in" : void 0;
}
const prefersReducedMotion = typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const prevEffectiveRef = React.useRef(effectiveAnimate);
React.useEffect(() => {
if (effectiveAnimate !== prevEffectiveRef.current) {
completedCountRef.current = 0;
prevEffectiveRef.current = effectiveAnimate;
if (prefersReducedMotion) {
setIsAnimating(false);
onAnimationComplete?.(effectiveAnimate);
if (animate === "loop") {
const delayMs = loopDelay ?? 2e3;
if (loopTimerRef.current) {
clearTimeout(loopTimerRef.current);
}
loopTimerRef.current = setTimeout(() => {
setLoopPhase((prev) => prev === "in" ? "out" : "in");
}, delayMs);
}
} else {
setIsAnimating(true);
}
}
}, [effectiveAnimate, prefersReducedMotion, onAnimationComplete, animate, loopDelay]);
const getStyles = core.useStyles({
name: "TextAnimate",
props,
classes: TextAnimate_module,
className,
style,
classNames,
styles,
unstyled,
vars,
varsResolver
});
let segments = [];
switch (by) {
case "word":
segments = children.split(/(\s+)/);
break;
case "character":
segments = children.split("");
break;
case "line":
segments = children.split("\n");
break;
case "text":
default:
segments = [children];
break;
}
const handleOnAnimationStart = React.useCallback(() => {
setIsAnimating(true);
onAnimationStart?.(effectiveAnimate);
}, [onAnimationStart, effectiveAnimate]);
const handleOnAnimationEnd = React.useCallback(() => {
onAnimationEnd?.(effectiveAnimate);
completedCountRef.current += 1;
if (completedCountRef.current === segments.length) {
setIsAnimating(false);
onAnimationComplete?.(effectiveAnimate);
if (animate === "loop") {
const delayMs = loopDelay ?? 2e3;
if (loopTimerRef.current) {
clearTimeout(loopTimerRef.current);
}
loopTimerRef.current = setTimeout(() => {
setLoopPhase((prev) => prev === "in" ? "out" : "in");
}, delayMs);
}
}
}, [onAnimationEnd, onAnimationComplete, effectiveAnimate, segments.length, animate, loopDelay]);
if (effectiveAnimate === "none" || effectiveAnimate === false || effectiveAnimate === void 0) {
return /* @__PURE__ */ React.createElement(core.Box, { ref: mergedRef, ...getStyles("root"), style: containerStyles, "aria-live": "polite" }, /* @__PURE__ */ React.createElement(core.Text, { component: "span", ...others, style: { visibility: "hidden" } }, children));
}
if (effectiveAnimate === "static") {
return /* @__PURE__ */ React.createElement(core.Box, { ref: mergedRef, ...getStyles("root"), style: containerStyles, "aria-live": "polite" }, /* @__PURE__ */ React.createElement(core.Text, { component: "span", ...others }, children));
}
return /* @__PURE__ */ React.createElement(core.Box, { ref: mergedRef, ...getStyles("root", { style: containerStyles }), "aria-live": "polite" }, segments.map((segment, i) => /* @__PURE__ */ React.createElement(
core.Text,
{
"data-text-animate": effectiveAnimate,
"data-text-animate-animation": animation,
"data-animating": isAnimating || void 0,
key: `${by}-${effectiveAnimate}-${i}`,
...getStyles("segment", {
style: {
...by === "line" ? { display: "block", whiteSpace: "normal" } : {},
animationDelay: `${(delay ?? 0) + i * staggerTiming}s`,
animationDuration: `${duration}s`,
animationFillMode: "forwards",
animationDirection: effectiveAnimate === "in" ? "normal" : "reverse"
}
}),
component: "span",
onAnimationStart: handleOnAnimationStart,
onAnimationEnd: handleOnAnimationEnd,
...others
},
segment
)));
});
TextAnimate.classes = TextAnimate_module;
TextAnimate.displayName = "TextAnimate";
TextAnimate.Typewriter = Typewriter.Typewriter;
TextAnimate.Spinner = Spinner.Spinner;
TextAnimate.NumberTicker = NumberTicker.NumberTicker;
TextAnimate.TextTicker = TextTicker.TextTicker;
TextAnimate.Gradient = Gradient.Gradient;
TextAnimate.Highlight = Highlight.Highlight;
TextAnimate.SplitFlap = SplitFlap.SplitFlap;
TextAnimate.Morphing = Morphing.Morphing;
TextAnimate.RotatingText = RotatingText.RotatingText;
exports.TextAnimate = TextAnimate;
//# sourceMappingURL=TextAnimate.cjs.map