import React, { useCallback, useEffect, useRef, useState } from "react"; import { Animated, Easing, Pressable, StyleProp, StyleSheet, Text, TextStyle, View, ViewStyle, } from "react-native"; interface SnackbarAction { label: string; onPress: () => void; } interface SnackbarProps { visible: boolean; message: string; action?: SnackbarAction; duration?: number; // 0 = sticky onDismiss?: () => void; backgroundColor?: string; textColor?: string; actionColor?: string; bottomOffset?: number; style?: StyleProp; textStyle?: StyleProp; accessibilityLabel?: string; } const Snackbar: React.FC = ({ visible, message, action, duration = 4000, onDismiss, backgroundColor = "#1F2937", textColor = "#FFFFFF", actionColor = "#60A5FA", bottomOffset = 24, style, textStyle, accessibilityLabel, }) => { const translate = useRef(new Animated.Value(120)).current; const opacity = useRef(new Animated.Value(0)).current; const [rendered, setRendered] = useState(visible); const timerRef = useRef | null>(null); const hide = useCallback(() => { Animated.parallel([ Animated.timing(translate, { toValue: 120, duration: 200, easing: Easing.in(Easing.ease), useNativeDriver: true, }), Animated.timing(opacity, { toValue: 0, duration: 200, useNativeDriver: true, }), ]).start(({ finished }) => { if (finished) { setRendered(false); onDismiss?.(); } }); }, [translate, opacity, onDismiss]); useEffect(() => { if (visible) { setRendered(true); translate.setValue(120); Animated.parallel([ Animated.timing(translate, { toValue: 0, duration: 240, easing: Easing.out(Easing.cubic), useNativeDriver: true, }), Animated.timing(opacity, { toValue: 1, duration: 240, useNativeDriver: true, }), ]).start(); if (duration > 0) { if (timerRef.current) clearTimeout(timerRef.current); timerRef.current = setTimeout(hide, duration); } } else if (rendered) { hide(); } return () => { if (timerRef.current) clearTimeout(timerRef.current); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [visible, duration]); if (!rendered) return null; return ( {message} {action && ( { action.onPress(); hide(); }} accessibilityRole="button" accessibilityLabel={action.label} hitSlop={8} style={styles.action} > {action.label.toUpperCase()} )} ); }; const styles = StyleSheet.create({ wrap: { position: "absolute", left: 16, right: 16, zIndex: 9999, }, snack: { flexDirection: "row", alignItems: "center", paddingVertical: 12, paddingHorizontal: 16, borderRadius: 6, shadowColor: "#000", shadowOffset: { width: 0, height: 3 }, shadowOpacity: 0.18, shadowRadius: 6, elevation: 6, }, text: { flex: 1, fontSize: 14, lineHeight: 20, }, action: { marginLeft: 12, paddingHorizontal: 4, }, actionText: { fontSize: 13, fontWeight: "700", letterSpacing: 0.5, }, }); export default Snackbar;