UNPKG

@erosnicolau/animated-text

Version:

Advanced React animated text component with comprehensive animation effects

218 lines (217 loc) 8.98 kB
import { jsx as _jsx } from "react/jsx-runtime"; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { AnimatedTextGroupContext } from '../contexts/AnimatedTextGroupContext'; export const AnimatedTextGroupProvider = ({ children, groupId, groupReplayKey, // Add this parameter coordinationMode, groupStartDelay, staggerDelay, groupEasing, debugMode, reduceMotionMode, isShowReplay, isShowConfig, onGroupStart, onGroupComplete, onChildStart, onChildComplete, logTiming }) => { // Group state - remove internal groupReplayKey since we get it from props const [_isGroupActive, _setIsGroupActive] = useState(false); const [activeChildren, setActiveChildren] = useState(new Set()); // Refs for callbacks and timing const childrenOrderRef = useRef([]); const registeredChildrenRef = useRef(new Map()); const previousGroupReplayKeyRef = useRef(groupReplayKey); const startTimeRef = useRef(0); const groupStartedRef = useRef(false); const isMountedRef = useRef(true); // Use refs for callback stability const onGroupStartRef = useRef(onGroupStart); const onGroupCompleteRef = useRef(onGroupComplete); const onChildStartRef = useRef(onChildStart); const onChildCompleteRef = useRef(onChildComplete); // Update refs when callbacks change useEffect(() => { onGroupStartRef.current = onGroupStart; }, [onGroupStart]); useEffect(() => { onGroupCompleteRef.current = onGroupComplete; }, [onGroupComplete]); useEffect(() => { onChildStartRef.current = onChildStart; }, [onChildStart]); useEffect(() => { onChildCompleteRef.current = onChildComplete; }, [onChildComplete]); // Track component mount status useEffect(() => { isMountedRef.current = true; return () => { isMountedRef.current = false; }; }, []); // Register a child component const registerChild = useCallback((childId, config) => { // Only register if component is still mounted if (!isMountedRef.current) return () => { }; // Update ref directly registeredChildrenRef.current.set(childId, config); // Track order of registration for coordination modes if (!childrenOrderRef.current.includes(childId)) { childrenOrderRef.current.push(childId); } // Return cleanup function return () => { // Only cleanup if component is still mounted if (!isMountedRef.current) return; // Update ref directly registeredChildrenRef.current.delete(childId); setActiveChildren(prev => { const newSet = new Set(prev); newSet.delete(childId); return newSet; }); // Remove from order tracking childrenOrderRef.current = childrenOrderRef.current.filter(id => id !== childId); }; }, [] // Keep empty to prevent infinite recreations ); // Get timing configuration for a specific child const getChildTiming = useCallback((childId) => { const childIndex = childrenOrderRef.current.indexOf(childId); const childConfig = registeredChildrenRef.current.get(childId); let childDelay = 0; switch (coordinationMode) { case 'parallel': // All children start at the same time (with their individual delays) childDelay = childConfig?.startDelay || 0; break; case 'sequential': { // Each child waits for the previous to complete let totalPreviousDuration = 0; for (let i = 0; i < childIndex; i++) { const prevChildId = childrenOrderRef.current[i]; if (prevChildId) { const prevConfig = registeredChildrenRef.current.get(prevChildId); if (prevConfig) { totalPreviousDuration += prevConfig.estimatedDuration + staggerDelay; } } } childDelay = totalPreviousDuration + (childConfig?.startDelay || 0); break; } case 'cascade': // Incremental delays for each child childDelay = childIndex * staggerDelay + (childConfig?.startDelay || 0); break; case 'wave': { // Wave pattern - children in groups const waveIndex = Math.floor(childIndex / 3); // Groups of 3 childDelay = waveIndex * staggerDelay + (childConfig?.startDelay || 0); break; } default: childDelay = childConfig?.startDelay || 0; } // Detect if this is a group replay by checking if groupReplayKey increased const isGroupReplay = groupReplayKey > previousGroupReplayKeyRef.current; // For group replays, ignore the group start delay but keep all other timing const effectiveGroupStartDelay = isGroupReplay ? 0 : groupStartDelay; return { groupStartDelay: effectiveGroupStartDelay, childDelay, staggerDelay, coordinationMode }; }, [coordinationMode, groupStartDelay, staggerDelay, groupReplayKey] // Added groupReplayKey dependency ); // Notify when a child starts const notifyChildStart = useCallback((childId) => { // Only notify if component is still mounted if (!isMountedRef.current) return; if (!groupStartedRef.current) { groupStartedRef.current = true; startTimeRef.current = performance.now(); // Use ref to avoid dependency issues setTimeout(() => onGroupStartRef.current?.(), 0); } setActiveChildren(prev => new Set([...prev, childId])); // Use ref to avoid dependency issues setTimeout(() => onChildStartRef.current?.(childId), 0); }, [] // Remove dependencies to prevent infinite loops ); // Notify when a child completes const notifyChildComplete = useCallback((childId) => { // Only notify if component is still mounted if (!isMountedRef.current) return; setActiveChildren(prev => { const newSet = new Set(prev); newSet.delete(childId); // Check if this was the last active child if (newSet.size === 0 && groupStartedRef.current) { // All children completed - delay to avoid state update during render setTimeout(() => { if (isMountedRef.current) { onGroupCompleteRef.current?.(); } }, 0); } return newSet; }); // Use ref to avoid dependency issues setTimeout(() => { if (isMountedRef.current) { onChildCompleteRef.current?.(childId); } }, 0); }, [] // Remove dependencies to prevent infinite loops ); // Reset group state when replay key changes useEffect(() => { setActiveChildren(new Set()); groupStartedRef.current = false; startTimeRef.current = 0; // Update the previous replay key after a short delay to ensure all components process the replay first setTimeout(() => { previousGroupReplayKeyRef.current = groupReplayKey; }, 100); // Debug logging to verify replay functionality if (debugMode && logTiming) { console.log(`[AnimatedTextGroup:${groupId}] Group reset triggered, replay key:`, groupReplayKey); } }, [groupReplayKey, groupId, debugMode, logTiming]); // Context value - memoized to prevent unnecessary re-renders const contextValue = useMemo(() => ({ // Group State groupId, isGroupActive: activeChildren.size > 0, groupReplayKey, // Child Registration & Discovery registerChild, getChildTiming, // Coordination coordinationMode, groupStartDelay, staggerDelay, ...(groupEasing && { groupEasing }), // Events notifyChildStart, notifyChildComplete, // Configuration debugMode, reduceMotionMode, // Group Controls isShowReplay, isShowConfig }), [ groupId, activeChildren.size, groupReplayKey, registerChild, getChildTiming, coordinationMode, groupStartDelay, staggerDelay, groupEasing, notifyChildStart, notifyChildComplete, debugMode, reduceMotionMode, isShowReplay, isShowConfig ]); return _jsx(AnimatedTextGroupContext.Provider, { value: contextValue, children: children }); };