Spaces:
Running
Running
| import React, { useEffect, useState } from 'react'; | |
| interface FlyToHistoryProps { | |
| onComplete: () => void; | |
| targetRef: React.RefObject<HTMLElement | null>; | |
| mode: 'vc' | 'tts'; | |
| } | |
| const FlyToHistory: React.FC<FlyToHistoryProps> = ({ onComplete, targetRef, mode }) => { | |
| const [style, setStyle] = useState<React.CSSProperties>({ | |
| top: '50%', | |
| left: '50%', | |
| transform: 'translate(-50%, -50%) scale(1.5)', | |
| opacity: 0, | |
| }); | |
| const isVoice = mode === 'vc'; | |
| // Distinct visuals for Voice vs Text | |
| const gradientClass = isVoice | |
| ? 'from-blue-600 via-indigo-500 to-purple-600' | |
| : 'from-teal-400 via-emerald-500 to-cyan-600'; | |
| const shadowColor = isVoice | |
| ? 'rgba(79, 70, 229, 0.6)' | |
| : 'rgba(16, 185, 129, 0.6)'; | |
| // Distinct icons | |
| const iconClass = isVoice ? 'fa-microphone-lines' : 'fa-keyboard'; | |
| useEffect(() => { | |
| // Initial reveal | |
| requestAnimationFrame(() => { | |
| setStyle(prev => ({ ...prev, opacity: 1, transform: 'translate(-50%, -50%) scale(1)' })); | |
| }); | |
| if (!targetRef.current) { | |
| setTimeout(onComplete, 800); | |
| return; | |
| } | |
| // 1. Get Target Coordinates | |
| const targetRect = targetRef.current.getBoundingClientRect(); | |
| const targetX = targetRect.left + targetRect.width / 2; | |
| const targetY = targetRect.top + targetRect.height / 2; | |
| // 2. Start Flight Animation | |
| const animationTimeout = setTimeout(() => { | |
| setStyle({ | |
| top: `${targetY}px`, | |
| left: `${targetX}px`, | |
| // Shrink, rotate, and fade out as it hits the target | |
| transform: 'translate(-50%, -50%) scale(0.1) rotate(-180deg)', | |
| opacity: 0.1, | |
| }); | |
| }, 250); // Small pause for user to recognize the icon | |
| // 3. Cleanup | |
| const endTimeout = setTimeout(() => { | |
| onComplete(); | |
| }, 1000); | |
| return () => { | |
| clearTimeout(animationTimeout); | |
| clearTimeout(endTimeout); | |
| }; | |
| }, [targetRef, onComplete]); | |
| return ( | |
| <> | |
| {/* Moving Object */} | |
| <div | |
| style={{ | |
| ...style, | |
| transition: 'all 0.75s cubic-bezier(0.175, 0.885, 0.32, 1.275)', // Bouncy landing curve | |
| position: 'fixed', | |
| zIndex: 100, | |
| }} | |
| className="pointer-events-none flex items-center justify-center will-change-transform" | |
| > | |
| {/* Main Icon Orb */} | |
| <div | |
| className={`relative w-20 h-20 bg-gradient-to-br ${gradientClass} rounded-2xl flex items-center justify-center z-20 border-2 border-white/40 backdrop-blur-sm`} | |
| style={{ boxShadow: `0 10px 40px ${shadowColor}, inset 0 0 20px rgba(255,255,255,0.3)` }} | |
| > | |
| <i className={`fas ${iconClass} text-3xl text-white drop-shadow-md`}></i> | |
| {/* Inner Ping Effect */} | |
| <div className="absolute inset-0 border-4 border-white/20 rounded-2xl animate-[ping_1.5s_infinite]"></div> | |
| {/* Sparkles */} | |
| <div className="absolute -top-2 -right-2 w-4 h-4 bg-white rounded-full animate-pulse shadow-[0_0_15px_white]"></div> | |
| </div> | |
| {/* Trail Effect */} | |
| <div | |
| className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-48 h-16 bg-gradient-to-r from-transparent via-white/30 to-transparent blur-xl rounded-full -z-10 animate-pulse" | |
| style={{ transform: 'translate(-50%, -50%) rotate(-45deg)' }} | |
| ></div> | |
| </div> | |
| {/* Screen Flash Overlay */} | |
| <div className="fixed inset-0 bg-white/30 z-[90] animate-[fadeOut_0.5s_ease-out] pointer-events-none"></div> | |
| </> | |
| ); | |
| }; | |
| export default FlyToHistory; |