@erosnicolau/animated-text
Version:
Advanced React animated text component with comprehensive animation effects
218 lines (217 loc) • 8.98 kB
JavaScript
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 });
};