/** * Voice Floating Action Button Component * * A floating action button for toggling voice listening mode. * Tap to start/stop listening. * Hidden when a call is already active. */ import React, { useRef, useEffect } from 'react'; import { StyleSheet, TouchableOpacity, Animated, ViewStyle, } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import * as Haptics from 'expo-haptics'; import { AppColors, BorderRadius } from '@/constants/theme'; import { useVoiceCall } from '@/contexts/VoiceCallContext'; interface VoiceFABProps { onPress: () => void; style?: ViewStyle; disabled?: boolean; isListening?: boolean; } const FAB_SIZE = 56; export function VoiceFAB({ onPress, style, disabled = false, isListening = false }: VoiceFABProps) { const { isCallActive } = useVoiceCall(); const insets = useSafeAreaInsets(); // Animation values const scale = useRef(new Animated.Value(1)).current; const opacity = useRef(new Animated.Value(1)).current; const pulseScale = useRef(new Animated.Value(1)).current; const pulseOpacity = useRef(new Animated.Value(0)).current; // Hide FAB when call is active useEffect(() => { if (isCallActive) { Animated.parallel([ Animated.timing(scale, { toValue: 0, duration: 200, useNativeDriver: true, }), Animated.timing(opacity, { toValue: 0, duration: 200, useNativeDriver: true, }), ]).start(); } else { Animated.parallel([ Animated.spring(scale, { toValue: 1, friction: 5, tension: 40, useNativeDriver: true, }), Animated.timing(opacity, { toValue: 1, duration: 200, useNativeDriver: true, }), ]).start(); } }, [isCallActive, scale, opacity]); // Pulse animation when listening useEffect(() => { if (isListening && !isCallActive) { // Start pulsing animation const pulseAnimation = Animated.loop( Animated.sequence([ Animated.parallel([ Animated.timing(pulseScale, { toValue: 1.8, duration: 1000, useNativeDriver: true, }), Animated.timing(pulseOpacity, { toValue: 0, duration: 1000, useNativeDriver: true, }), ]), Animated.parallel([ Animated.timing(pulseScale, { toValue: 1, duration: 0, useNativeDriver: true, }), Animated.timing(pulseOpacity, { toValue: 0.6, duration: 0, useNativeDriver: true, }), ]), ]) ); pulseAnimation.start(); return () => { pulseAnimation.stop(); pulseScale.setValue(1); pulseOpacity.setValue(0); }; } else { pulseScale.setValue(1); pulseOpacity.setValue(0); } }, [isListening, isCallActive, pulseScale, pulseOpacity]); // Press animation with haptic feedback const handlePressIn = () => { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); Animated.spring(scale, { toValue: 0.9, friction: 5, useNativeDriver: true, }).start(); }; const handlePressOut = () => { Animated.spring(scale, { toValue: 1, friction: 5, useNativeDriver: true, }).start(); }; // Don't render if call is active if (isCallActive) { return null; } return ( {/* Pulse ring when listening */} {isListening && ( )} ); } const styles = StyleSheet.create({ container: { position: 'absolute', left: 0, right: 0, alignItems: 'center', zIndex: 100, }, pulseRing: { position: 'absolute', width: FAB_SIZE, height: FAB_SIZE, borderRadius: BorderRadius.full, backgroundColor: AppColors.error, }, fab: { width: FAB_SIZE, height: FAB_SIZE, borderRadius: BorderRadius.full, backgroundColor: AppColors.success, justifyContent: 'center', alignItems: 'center', shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.3, shadowRadius: 8, elevation: 8, }, fabListening: { backgroundColor: AppColors.error, }, fabDisabled: { backgroundColor: AppColors.surface, shadowOpacity: 0.1, }, });