File size: 4,562 Bytes
403a2e1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
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<HTMLDivElement | null>;
  coachBubbleContentRef: RefObject<HTMLDivElement | null>;

  // 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<HTMLButtonElement | null>
): UseCoachModalAnimationReturn {
  const [isClosingModal, setIsClosingModal] = useState(false);
  const [animationTarget, setAnimationTarget] = useState<AnimationTarget>({ x: 0, y: 0 });
  const [tailTarget, setTailTarget] = useState<AnimationTarget | null>(null);

  const coachBubbleRef = useRef<HTMLDivElement>(null);
  const coachBubbleContentRef = useRef<HTMLDivElement>(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,
  };
}