import React, { useCallback, useEffect, useMemo, useRef } from "react"; import { Animated, Dimensions, Easing, Modal, PanResponder, Pressable, StyleSheet, View, } from "react-native"; import { useResponsiveScale } from "../../hooks/useResponsiveScale"; import { useSafeAreaInsets } from "react-native-safe-area-context"; const DISMISS_DRAG_RATIO = 0.22; const DISMISS_VELOCITY = 1.2; const DEFAULT_HEIGHT_RATIO = 0.75; type AnimatedBottomSheetProps = { visible: boolean; onClose: () => void; children: React.ReactNode; heightRatio?: number; backdropLabel?: string; }; const AnimatedBottomSheet = ({ visible, onClose, children, heightRatio = DEFAULT_HEIGHT_RATIO, backdropLabel = "Close", }: AnimatedBottomSheetProps) => { const { rs } = useResponsiveScale(); const insets = useSafeAreaInsets(); const styles = useMemo(() => createStyles(rs), [rs]); const sheetMaxHeight = useMemo( () => Dimensions.get("window").height * heightRatio, [heightRatio], ); const translateY = useRef(new Animated.Value(0)).current; const dragY = useRef(new Animated.Value(0)).current; const backdropOpacity = useRef(new Animated.Value(0)).current; const sheetStyle = useMemo( () => ({ maxHeight: sheetMaxHeight, height: sheetMaxHeight, paddingBottom: Math.max(insets.bottom, rs(16)), }), [insets.bottom, rs, sheetMaxHeight], ); const sheetTranslateY = useMemo( () => Animated.add(translateY, dragY), [translateY, dragY], ); const resetSheetPosition = useCallback(() => { dragY.setValue(0); translateY.setValue(sheetMaxHeight); backdropOpacity.setValue(0); }, [backdropOpacity, dragY, sheetMaxHeight, translateY]); const animateSheetClosed = useCallback(() => { Animated.parallel([ Animated.timing(backdropOpacity, { toValue: 0, duration: 200, useNativeDriver: true, }), Animated.timing(translateY, { toValue: sheetMaxHeight, duration: 250, easing: Easing.in(Easing.cubic), useNativeDriver: true, }), ]).start(({ finished }) => { if (finished) { resetSheetPosition(); onClose(); } }); }, [ backdropOpacity, onClose, resetSheetPosition, sheetMaxHeight, translateY, ]); const closeWithAnimation = useCallback(() => { translateY.stopAnimation((baseY) => { dragY.stopAnimation((offsetY) => { dragY.setValue(0); translateY.setValue(baseY + offsetY); animateSheetClosed(); }); }); }, [animateSheetClosed, dragY, translateY]); useEffect(() => { if (!visible) { return; } dragY.setValue(0); translateY.setValue(sheetMaxHeight); backdropOpacity.setValue(0); Animated.parallel([ Animated.timing(backdropOpacity, { toValue: 1, duration: 250, useNativeDriver: true, }), Animated.spring(translateY, { toValue: 0, useNativeDriver: true, damping: 22, stiffness: 220, }), ]).start(); }, [visible, backdropOpacity, dragY, sheetMaxHeight, translateY]); const panResponder = useMemo( () => PanResponder.create({ onStartShouldSetPanResponder: () => true, onMoveShouldSetPanResponder: (_, gesture) => gesture.dy > 6 && Math.abs(gesture.dy) > Math.abs(gesture.dx), onPanResponderMove: (_, gesture) => { if (gesture.dy > 0) { dragY.setValue(gesture.dy); } }, onPanResponderRelease: (_, gesture) => { const shouldDismiss = gesture.dy > sheetMaxHeight * DISMISS_DRAG_RATIO || gesture.vy > DISMISS_VELOCITY; if (shouldDismiss) { closeWithAnimation(); return; } Animated.spring(dragY, { toValue: 0, useNativeDriver: true, damping: 20, stiffness: 300, }).start(); }, onPanResponderTerminate: () => { Animated.spring(dragY, { toValue: 0, useNativeDriver: true, }).start(); }, }), [closeWithAnimation, dragY, sheetMaxHeight], ); return ( {children} ); }; export default AnimatedBottomSheet; const createStyles = (rs: (value: number) => number) => StyleSheet.create({ root: { flex: 1, }, backdropDim: { backgroundColor: "rgba(0, 0, 0, 0.45)", }, sheetAlign: { ...StyleSheet.absoluteFillObject, justifyContent: "flex-end", }, sheet: { backgroundColor: "#FFFFFF", borderTopLeftRadius: rs(20), borderTopRightRadius: rs(20), paddingHorizontal: rs(18), paddingTop: rs(10), overflow: "hidden", flexDirection: "column", width: "100%", }, handleHitArea: { alignItems: "center", paddingVertical: rs(8), marginBottom: rs(12), }, handle: { width: rs(60), height: rs(5), borderRadius: rs(8), backgroundColor: "#E6EAF0", }, content: { flex: 1, minHeight: 0, }, });