/ components / FloatingActionButton.tsx
FloatingActionButton.tsx
  1  import React, { useRef, useState } from 'react';
  2  import {
  3    View,
  4    TouchableOpacity,
  5    Animated,
  6    StyleSheet,
  7    ViewStyle,
  8  } from 'react-native';
  9  import { Ionicons } from '@expo/vector-icons';
 10  import { useTheme } from '@/constants/ThemeContext';
 11  import { Colors } from '@/constants/Colors';
 12  import { useHapticFeedback } from '@/utils/haptics';
 13  import { useSafeAreaInsets } from 'react-native-safe-area-context';
 14  
 15  interface FloatingActionButtonProps {
 16    onPress?: () => void;
 17    onScrollToTop?: () => void;
 18    onRefresh?: () => void;
 19    style?: ViewStyle;
 20    visible?: boolean;
 21  }
 22  
 23  export const FloatingActionButton: React.FC<FloatingActionButtonProps> = ({
 24    onPress,
 25    onScrollToTop,
 26    onRefresh,
 27    style,
 28    visible = true,
 29  }) => {
 30    const { actualTheme } = useTheme();
 31    const colors = Colors[actualTheme];
 32    const haptics = useHapticFeedback();
 33    const insets = useSafeAreaInsets();
 34    const [expanded, setExpanded] = useState(false);
 35  
 36    const scaleAnim = useRef(new Animated.Value(1)).current;
 37    const rotateAnim = useRef(new Animated.Value(0)).current;
 38    const actionScale1 = useRef(new Animated.Value(0)).current;
 39    const actionScale2 = useRef(new Animated.Value(0)).current;
 40  
 41    const toggleExpanded = () => {
 42      haptics.onPress();
 43      setExpanded(!expanded);
 44  
 45      Animated.parallel([
 46        Animated.spring(rotateAnim, {
 47          toValue: expanded ? 0 : 1,
 48          useNativeDriver: true,
 49        }),
 50        Animated.stagger(50, [
 51          Animated.spring(actionScale1, {
 52            toValue: expanded ? 0 : 1,
 53            useNativeDriver: true,
 54          }),
 55          Animated.spring(actionScale2, {
 56            toValue: expanded ? 0 : 1,
 57            useNativeDriver: true,
 58          }),
 59        ]),
 60      ]).start();
 61    };
 62  
 63    const handleActionPress = (action: () => void) => {
 64      haptics.onSelection();
 65      action();
 66      toggleExpanded();
 67    };
 68  
 69    const handleMainPress = () => {
 70      if (expanded) {
 71        toggleExpanded();
 72      } else {
 73        haptics.onPress();
 74        onPress?.();
 75      }
 76    };
 77  
 78    const rotation = rotateAnim.interpolate({
 79      inputRange: [0, 1],
 80      outputRange: ['0deg', '45deg'],
 81    });
 82  
 83    if (!visible) return null;
 84  
 85    return (
 86      <View style={[styles.container, { bottom: insets.bottom + 100 }, style]}>
 87        {/* Action Buttons */}
 88        {onScrollToTop && (
 89          <Animated.View
 90            style={[
 91              styles.actionButton,
 92              {
 93                transform: [{ scale: actionScale1 }],
 94                backgroundColor: colors.card,
 95              },
 96            ]}
 97          >
 98            <TouchableOpacity
 99              onPress={() => handleActionPress(onScrollToTop)}
100              style={styles.actionTouchable}
101            >
102              <Ionicons name="arrow-up" size={20} color={colors.primary} />
103            </TouchableOpacity>
104          </Animated.View>
105        )}
106  
107        {onRefresh && (
108          <Animated.View
109            style={[
110              styles.actionButton,
111              {
112                transform: [{ scale: actionScale2 }],
113                backgroundColor: colors.card,
114              },
115            ]}
116          >
117            <TouchableOpacity
118              onPress={() => handleActionPress(onRefresh)}
119              style={styles.actionTouchable}
120            >
121              <Ionicons name="refresh" size={20} color={colors.primary} />
122            </TouchableOpacity>
123          </Animated.View>
124        )}
125  
126        {/* Main Button */}
127        <Animated.View
128          style={[
129            styles.mainButton,
130            {
131              backgroundColor: colors.primary,
132              transform: [{ scale: scaleAnim }],
133            },
134          ]}
135        >
136          <TouchableOpacity
137            onPress={handleMainPress}
138            onPressIn={() => {
139              Animated.spring(scaleAnim, {
140                toValue: 0.9,
141                useNativeDriver: true,
142              }).start();
143            }}
144            onPressOut={() => {
145              Animated.spring(scaleAnim, {
146                toValue: 1,
147                useNativeDriver: true,
148              }).start();
149            }}
150            style={styles.mainTouchable}
151          >
152            <Animated.View style={{ transform: [{ rotate: rotation }] }}>
153              <Ionicons name="add" size={24} color="#FFFFFF" />
154            </Animated.View>
155          </TouchableOpacity>
156        </Animated.View>
157      </View>
158    );
159  };
160  
161  const styles = StyleSheet.create({
162    container: {
163      position: 'absolute',
164      right: 20,
165      alignItems: 'center',
166    },
167    mainButton: {
168      width: 56,
169      height: 56,
170      borderRadius: 28,
171      elevation: 8,
172      shadowColor: '#000',
173      shadowOffset: {
174        width: 0,
175        height: 4,
176      },
177      shadowOpacity: 0.3,
178      shadowRadius: 8,
179    },
180    mainTouchable: {
181      flex: 1,
182      justifyContent: 'center',
183      alignItems: 'center',
184    },
185    actionButton: {
186      width: 44,
187      height: 44,
188      borderRadius: 22,
189      marginBottom: 12,
190      elevation: 4,
191      shadowColor: '#000',
192      shadowOffset: {
193        width: 0,
194        height: 2,
195      },
196      shadowOpacity: 0.2,
197      shadowRadius: 4,
198    },
199    actionTouchable: {
200      flex: 1,
201      justifyContent: 'center',
202      alignItems: 'center',
203    },
204  });