import { Text } from '@/components/ui/text';
import { View } from '@/components/ui/view';
import { useColor } from '@/hooks/useColor';
import { CORNERS, FONT_SIZE } from '@/theme/globals';
import React, { useEffect, useState } from 'react';
import {
ActionSheetIOS,
Dimensions,
Modal,
Platform,
Pressable,
ScrollView,
StyleSheet,
TouchableOpacity,
ViewStyle,
} from 'react-native';
import Animated, {
Easing,
interpolate,
runOnJS,
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated';
export interface ActionSheetOption {
title: string;
onPress: () => void;
destructive?: boolean;
disabled?: boolean;
icon?: React.ReactNode;
}
interface ActionSheetProps {
visible: boolean;
onClose: () => void;
title?: string;
message?: string;
options: ActionSheetOption[];
cancelButtonTitle?: string;
style?: ViewStyle;
}
export function ActionSheet({
visible,
onClose,
title,
message,
options,
cancelButtonTitle = 'Cancel',
style,
}: ActionSheetProps) {
// Use iOS native ActionSheet on iOS
if (Platform.OS === 'ios') {
useEffect(() => {
if (visible) {
const optionTitles = options.map((option) => option.title);
const destructiveButtonIndex = options.findIndex(
(option) => option.destructive
);
const disabledButtonIndices = options
.map((option, index) => (option.disabled ? index : -1))
.filter((index) => index !== -1);
ActionSheetIOS.showActionSheetWithOptions(
{
title,
message,
options: [...optionTitles, cancelButtonTitle],
cancelButtonIndex: optionTitles.length,
destructiveButtonIndex:
destructiveButtonIndex !== -1
? destructiveButtonIndex
: undefined,
disabledButtonIndices:
disabledButtonIndices.length > 0
? disabledButtonIndices
: undefined,
},
(buttonIndex) => {
if (buttonIndex < optionTitles.length) {
options[buttonIndex].onPress();
}
onClose();
}
);
}
}, [visible, title, message, options, cancelButtonTitle, onClose]);
// Return null for iOS as we use the native ActionSheet
return null;
}
// Custom implementation for Android and other platforms
return (
);
}
// Custom ActionSheet implementation for Android using react-native-reanimated
function AndroidActionSheet({
visible,
onClose,
title,
message,
options,
cancelButtonTitle,
style,
}: ActionSheetProps) {
const [isSheetVisible, setIsSheetVisible] = useState(visible);
const progress = useSharedValue(0);
const screenHeight = Dimensions.get('window').height;
const cardColor = useColor('card');
const textColor = useColor('text');
const mutedColor = useColor('textMuted');
const borderColor = useColor('border');
const destructiveColor = useColor('red');
useEffect(() => {
if (visible) {
setIsSheetVisible(true);
progress.value = withTiming(1, {
duration: 300,
easing: Easing.out(Easing.quad),
});
} else {
// Animate out, then set the modal to invisible after the animation is done
progress.value = withTiming(
0,
{ duration: 250, easing: Easing.in(Easing.quad) },
(finished) => {
if (finished) {
runOnJS(setIsSheetVisible)(false);
}
}
);
}
}, [visible, progress]);
// Animated style for the backdrop
const backdropAnimatedStyle = useAnimatedStyle(() => ({
opacity: progress.value,
}));
// Animated style for the sheet itself (slide up/down)
const sheetAnimatedStyle = useAnimatedStyle(() => {
const translateY = interpolate(progress.value, [0, 1], [screenHeight, 0]);
return {
transform: [{ translateY }],
};
});
const handleOptionPress = (option: ActionSheetOption) => {
if (!option.disabled) {
option.onPress();
onClose();
}
};
const handleBackdropPress = () => {
onClose();
};
// Render null if the sheet is not supposed to be visible
if (!isSheetVisible) {
return null;
}
return (
{/* Header */}
{(title || message) && (
{title && (
{title}
)}
{message && (
{message}
)}
)}
{/* Options */}
{options.map((option, index) => (
handleOptionPress(option)}
disabled={option.disabled}
activeOpacity={0.6}
>
{option.icon && (
{option.icon}
)}
{option.title}
))}
{/* Cancel Button */}
{cancelButtonTitle}
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'flex-end',
},
backdrop: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0, 0, 0, 0.5)',
},
backdropPressable: {
flex: 1,
},
sheet: {
borderTopLeftRadius: CORNERS,
borderTopRightRadius: CORNERS,
paddingBottom: 34, // Safe area bottom padding
maxHeight: '80%',
elevation: 10,
shadowColor: '#000',
shadowOffset: {
width: 0,
height: -2,
},
shadowOpacity: 0.25,
shadowRadius: 10,
},
header: {
paddingHorizontal: 20,
paddingTop: 20,
paddingBottom: 16,
alignItems: 'center',
},
title: {
fontSize: 18,
fontWeight: '600',
textAlign: 'center',
marginBottom: 4,
},
message: {
fontSize: FONT_SIZE - 1,
textAlign: 'center',
lineHeight: 20,
},
optionsContainer: {
maxHeight: 300,
},
option: {
borderBottomWidth: StyleSheet.hairlineWidth,
paddingHorizontal: 20,
paddingVertical: 16,
},
lastOption: {
borderBottomWidth: 0,
},
disabledOption: {
opacity: 0.5,
},
optionContent: {
flexDirection: 'row',
alignItems: 'center',
},
optionIcon: {
marginRight: 12,
width: 24,
height: 24,
alignItems: 'center',
justifyContent: 'center',
},
optionText: {
fontSize: FONT_SIZE,
fontWeight: '500',
flex: 1,
},
cancelContainer: {
borderTopWidth: StyleSheet.hairlineWidth,
marginTop: 8,
},
cancelButton: {
paddingHorizontal: 20,
paddingVertical: 16,
alignItems: 'center',
},
cancelText: {
fontSize: FONT_SIZE,
fontWeight: '600',
},
});
// Hook for easier ActionSheet usage (No changes needed here)
export function useActionSheet() {
const [isVisible, setIsVisible] = React.useState(false);
const [config, setConfig] = React.useState<
Omit
>({
options: [],
});
const show = React.useCallback(
(actionSheetConfig: Omit) => {
setConfig(actionSheetConfig);
setIsVisible(true);
},
[]
);
const hide = React.useCallback(() => {
setIsVisible(false);
}, []);
const ActionSheetComponent = React.useMemo(
() => ,
[isVisible, hide, config]
);
return {
show,
hide,
ActionSheet: ActionSheetComponent,
isVisible,
};
}