import { useState, useRef, useEffect, RefObject } from 'react'; interface AnimationTarget { x: number; y: number; } interface UseCoachModalAnimationReturn { // State isClosingModal: boolean; animationTarget: AnimationTarget; tailTarget: AnimationTarget | null; // Refs to attach to elements coachBubbleRef: RefObject; coachBubbleContentRef: RefObject; // Handlers handleCloseModal: () => void; // Animation class names getOverlayClassName: (baseClass: string) => string; getBubbleClassName: (baseClass: string) => string; getBubbleStyle: () => React.CSSProperties; } const ANIMATION_DURATION = 400; const ESTIMATED_BUBBLE_HEIGHT = 280; export function useCoachModalAnimation( showModal: boolean, setShowModal: (show: boolean) => void, triggerRef: RefObject ): UseCoachModalAnimationReturn { const [isClosingModal, setIsClosingModal] = useState(false); const [animationTarget, setAnimationTarget] = useState({ x: 0, y: 0 }); const [tailTarget, setTailTarget] = useState(null); const coachBubbleRef = useRef(null); const coachBubbleContentRef = useRef(null); // Calculate animation targets when modal opens useEffect(() => { if (showModal && !isClosingModal) { // Calculate animation target const buttonRect = triggerRef.current?.getBoundingClientRect(); const viewportCenterX = window.innerWidth / 2; const viewportCenterY = window.innerHeight / 2; if (buttonRect) { const buttonCenterX = buttonRect.left + buttonRect.width / 2; const buttonCenterY = buttonRect.top + buttonRect.height / 2; setAnimationTarget({ x: buttonCenterX - viewportCenterX, y: buttonCenterY - viewportCenterY, }); // Calculate tail target const estimatedBubbleWidth = Math.min(384, window.innerWidth - 32); const bubbleLeft = viewportCenterX - estimatedBubbleWidth / 2; const bubbleBottom = viewportCenterY + ESTIMATED_BUBBLE_HEIGHT / 2; const buttonTargetX = buttonRect.left + buttonRect.width * 0.5; const buttonTopY = buttonRect.top; setTailTarget({ x: buttonTargetX - bubbleLeft, y: buttonTopY - bubbleBottom, }); } } else if (!showModal) { setTailTarget(null); } }, [showModal, isClosingModal, triggerRef]); const handleCloseModal = () => { // Recalculate animation target for closing (bubble position may have changed) const buttonRect = triggerRef.current?.getBoundingClientRect(); const bubbleRect = coachBubbleRef.current?.getBoundingClientRect(); if (buttonRect && bubbleRect) { const buttonCenterX = buttonRect.left + buttonRect.width / 2; const buttonCenterY = buttonRect.top + buttonRect.height / 2; const bubbleCenterX = bubbleRect.left + bubbleRect.width / 2; const bubbleCenterY = bubbleRect.top + bubbleRect.height / 2; const targetX = buttonCenterX - bubbleCenterX; const targetY = buttonCenterY - bubbleCenterY; // Set CSS variables directly on element for immediate effect (before React re-renders) if (coachBubbleRef.current) { coachBubbleRef.current.style.setProperty('--translate-x', `${targetX}px`); coachBubbleRef.current.style.setProperty('--translate-y', `${targetY}px`); } // Also update state to prevent React from overwriting on re-render setAnimationTarget({ x: targetX, y: targetY }); } setIsClosingModal(true); // Wait for animation to complete before actually closing setTimeout(() => { setShowModal(false); setIsClosingModal(false); }, ANIMATION_DURATION); }; const getOverlayClassName = (baseClass: string): string => { return `${baseClass} ${isClosingModal ? 'animate-fade-out' : 'animate-fade-in'}`; }; const getBubbleClassName = (baseClass: string): string => { return `${baseClass} ${isClosingModal ? 'animate-shrink-to-coach-button' : 'animate-expand-from-coach-button'}`; }; const getBubbleStyle = (): React.CSSProperties => { return { '--translate-x': `${animationTarget.x}px`, '--translate-y': `${animationTarget.y}px`, } as React.CSSProperties; }; return { isClosingModal, animationTarget, tailTarget, coachBubbleRef, coachBubbleContentRef, handleCloseModal, getOverlayClassName, getBubbleClassName, getBubbleStyle, }; }