UNPKG

@gfazioli/mantine-text-animate

Version:

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

233 lines (229 loc) 8.1 kB
'use client'; 'use strict'; 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