/** * useSlideTransition Hook * * Manages slide transition animation for Gallery/Carousel elements. * Implements fade-through-overlay: Slide 1 -> fade out -> overlay -> fade in -> Slide 2 */ import { useState, useCallback, useRef, useEffect } from 'react'; import type { CSSProperties } from 'react'; import type { ResolvedSlideTransition } from '../lib/resolveSlideTransition'; // Transition phases: idle -> fadingOut -> fadingIn -> idle type TransitionPhase = 'idle' | 'fadingOut' | 'fadingIn'; interface SlideTransitionState { currentIndex: number; displayIndex: number; // What's actually shown (may differ during transition) phase: TransitionPhase; overlayOpacity: number; // 0 = hidden, 1 = fully visible } interface UseSlideTransitionReturn { /** Current logical index */ currentIndex: number; /** Index to display (follows currentIndex with delay during transition) */ displayIndex: number; /** Current transition phase */ phase: TransitionPhase; /** Whether any transition is active */ isTransitioning: boolean; /** Overlay opacity (0-1) */ overlayOpacity: number; /** Overlay color from settings */ overlayColor: string; /** Navigate to specific slide index */ goToIndex: (index: number) => void; /** Set initial index without transition */ setInitialIndex: (index: number) => void; /** CSS transition style for slide image */ slideTransitionStyle: CSSProperties; /** CSS transition style for overlay */ overlayTransitionStyle: CSSProperties; /** Current slide opacity */ slideOpacity: number; } export function useSlideTransition( settings: ResolvedSlideTransition, ): UseSlideTransitionReturn { const [state, setState] = useState({ currentIndex: 0, displayIndex: 0, phase: 'idle', overlayOpacity: 0, }); const timeoutRef = useRef | null>(null); const pendingIndexRef = useRef(null); // Cleanup timeout on unmount useEffect(() => { return () => { if (timeoutRef.current) clearTimeout(timeoutRef.current); }; }, []); // Half duration for each phase (fade out + fade in) const halfDuration = settings.durationMs / 2; const goToIndex = useCallback( (newIndex: number) => { if (newIndex === state.currentIndex && state.phase === 'idle') return; // Clear pending transition if (timeoutRef.current) clearTimeout(timeoutRef.current); if (settings.type === 'none') { // Instant switch - no transition setState({ currentIndex: newIndex, displayIndex: newIndex, phase: 'idle', overlayOpacity: 0, }); return; } // Store pending index pendingIndexRef.current = newIndex; // Phase 1: Fade out current slide (overlay fades in) setState((prev) => ({ ...prev, currentIndex: newIndex, phase: 'fadingOut', overlayOpacity: 1, })); // Phase 2: At midpoint, switch display to new slide, start fade in timeoutRef.current = setTimeout(() => { setState((prev) => ({ ...prev, displayIndex: pendingIndexRef.current ?? prev.currentIndex, phase: 'fadingIn', overlayOpacity: 0, })); // Phase 3: Complete transition timeoutRef.current = setTimeout(() => { setState((prev) => ({ ...prev, phase: 'idle', })); pendingIndexRef.current = null; }, halfDuration); }, halfDuration); }, [state.currentIndex, state.phase, settings.type, halfDuration], ); const setInitialIndex = useCallback((index: number) => { if (timeoutRef.current) clearTimeout(timeoutRef.current); pendingIndexRef.current = null; setState({ currentIndex: index, displayIndex: index, phase: 'idle', overlayOpacity: 0, }); }, []); // CSS transition styles const slideTransitionStyle: CSSProperties = settings.type === 'fade' ? { transition: `opacity ${halfDuration}ms ${settings.easing}` } : {}; const overlayTransitionStyle: CSSProperties = settings.type === 'fade' ? { transition: `opacity ${halfDuration}ms ${settings.easing}` } : {}; // Slide opacity: visible in idle, fades based on phase const slideOpacity = state.phase === 'fadingOut' ? 0 : 1; return { currentIndex: state.currentIndex, displayIndex: state.displayIndex, phase: state.phase, isTransitioning: state.phase !== 'idle', overlayOpacity: state.overlayOpacity, overlayColor: settings.overlayColor, goToIndex, setInitialIndex, slideTransitionStyle, overlayTransitionStyle, slideOpacity, }; }