|
import React, { createContext, useContext, useState, useCallback } from "react"; |
|
import { StyleSheet, Dimensions, Pressable } from "react-native"; |
|
import Animated, { |
|
useSharedValue, |
|
withTiming, |
|
useAnimatedStyle, |
|
Easing, |
|
runOnJS, |
|
} from "react-native-reanimated"; |
|
import { useSafeAreaInsets } from "react-native-safe-area-context"; |
|
|
|
const { height } = Dimensions.get("window"); |
|
|
|
const GlobalModalContext = createContext(); |
|
|
|
export const GlobalModalProvider = ({ children }) => { |
|
const [content, setContent] = useState(null); |
|
const [options, setOptions] = useState({}); |
|
const [visible, setVisible] = useState(false); |
|
const insets = useSafeAreaInsets(); |
|
|
|
// Animations |
|
const backdropOpacity = useSharedValue(0); |
|
const translateY = useSharedValue(-height); // start off-screen |
|
|
|
const showModal = useCallback((renderContent, modalOptions = {}) => { |
|
setContent(() => renderContent); |
|
setOptions(modalOptions); |
|
setVisible(true); |
|
|
|
// Animate in |
|
backdropOpacity.value = withTiming(1, { duration: 200, easing: Easing.ease }); |
|
translateY.value = withTiming(0, { duration: 300, easing: Easing.out(Easing.exp) }); |
|
}, []); |
|
|
|
const hideModal = useCallback(() => { |
|
// Animate out |
|
backdropOpacity.value = withTiming(0, { duration: 200, easing: Easing.ease }); |
|
translateY.value = withTiming( |
|
-height, |
|
{ duration: 300, easing: Easing.in(Easing.exp) }, |
|
(finished) => { |
|
if (finished) { |
|
runOnJS(setVisible)(false); |
|
runOnJS(setContent)(null); |
|
runOnJS(setOptions)({}); |
|
} |
|
} |
|
); |
|
}, []); |
|
|
|
// Backdrop fade style |
|
const backdropStyle = useAnimatedStyle(() => ({ |
|
opacity: backdropOpacity.value, |
|
})); |
|
|
|
// Slide in modal container |
|
const modalStyle = useAnimatedStyle(() => ({ |
|
transform: [{ translateY: translateY.value }], |
|
})); |
|
|
|
return ( |
|
<GlobalModalContext.Provider value={{ showModal, hideModal }}> |
|
{children} |
|
|
|
{visible && ( |
|
<Animated.View style={[styles.overlay, backdropStyle]}> |
|
{/* Backdrop click to dismiss */} |
|
<Pressable |
|
style={StyleSheet.absoluteFill} |
|
onPress={options.dismissOnBackdrop !== false ? hideModal : undefined} |
|
/> |
|
{/* Modal container (you control width/height via options.containerStyle) */} |
|
<Animated.View |
|
style={[ |
|
styles.modalContainer, |
|
modalStyle, |
|
options.containerStyle, // ✅ FULL CONTROL HERE |
|
{ paddingBottom: insets.bottom, paddingTop: insets.top }, |
|
]} |
|
> |
|
{content ? content({ hideModal }) : null} |
|
</Animated.View> |
|
</Animated.View> |
|
)} |
|
</GlobalModalContext.Provider> |
|
); |
|
}; |
|
|
|
export const useGlobalModal = () => useContext(GlobalModalContext); |
|
|
|
const styles = StyleSheet.create({ |
|
overlay: { |
|
...StyleSheet.absoluteFillObject, |
|
backgroundColor: "rgba(0,0,0,0.4)", |
|
justifyContent: "center", // Centered |
|
alignItems: "center", |
|
}, |
|
modalContainer: { |
|
backgroundColor: "#fff", |
|
borderRadius: 16, |
|
padding: 20, |
|
shadowColor: "#000", |
|
shadowOpacity: 0.2, |
|
shadowRadius: 10, |
|
elevation: 5, |
|
}, |
|
}); |