import * as React from 'react';
import { View, Dimensions } from 'react-native';
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
runOnJS,
withSpring
} from 'react-native-reanimated';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import { cn } from '@/lib/utils';
import { Text } from './text';
import { useToastStore, type Toast as ToastType } from '../../stores/toast';
import { useThemeColors } from '@/hooks/useThemeColors';
import { Info, AlertCircle, CheckCircle, AlertTriangle } from 'lucide-react-native';
const SCREEN_WIDTH = Dimensions.get('window').width;
const SWIPE_THRESHOLD = SCREEN_WIDTH * 0.3;
export function Toast({
id,
title,
description,
variant = 'default',
duration = 3000,
position = 'top',
direction = 'top'
}: ToastType) {
const { colors } = useThemeColors();
const removeToast = useToastStore((state) => state.removeToast);
// Initial values based on direction (now more subtle)
const getInitialTranslate = () => {
const offset = 20;
switch (direction) {
case 'left': return { x: -offset, y: 0 };
case 'right': return { x: offset, y: 0 };
case 'bottom': return { x: 0, y: offset };
case 'top':
default: return { x: 0, y: -offset };
}
};
const initial = getInitialTranslate();
const translateX = useSharedValue(initial.x);
const translateY = useSharedValue(initial.y);
const opacity = useSharedValue(0);
React.useEffect(() => {
// Entry animation - Simple withTiming
opacity.value = withTiming(1, { duration: 300 });
translateX.value = withTiming(0, { duration: 300 });
translateY.value = withTiming(0, { duration: 300 });
const timer = setTimeout(() => {
dismiss();
}, duration);
return () => clearTimeout(timer);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const dismiss = () => {
opacity.value = withTiming(0, { duration: 300 }, () => {
runOnJS(removeToast)(id);
});
};
const panGesture = Gesture.Pan()
.onUpdate((event) => {
translateX.value = event.translationX;
translateY.value = event.translationY;
})
.onEnd((event) => {
const shouldDismiss =
Math.abs(event.translationX) > SWIPE_THRESHOLD ||
Math.abs(event.translationY) > SWIPE_THRESHOLD;
if (shouldDismiss) {
const destX = event.translationX > 0 ? SCREEN_WIDTH : -SCREEN_WIDTH;
const destY = event.translationY > 0 ? 100 : -100;
// If horizontal swipe is stronger, exit horizontally
if (Math.abs(event.translationX) > Math.abs(event.translationY)) {
translateX.value = withTiming(destX, { duration: 300 }, () => runOnJS(removeToast)(id));
} else {
translateY.value = withTiming(destY, { duration: 300 }, () => runOnJS(removeToast)(id));
}
} else {
translateX.value = withTiming(0, { duration: 200 });
translateY.value = withTiming(0, { duration: 200 });
}
});
const animatedStyle = useAnimatedStyle(() => ({
transform: [
{ translateX: translateX.value },
{ translateY: translateY.value }
],
opacity: opacity.value,
}));
const variantStyles = {
default: 'bg-background/80 border-border',
success: 'bg-green-100/80 border-green-300 dark:bg-green-900/80 dark:border-green-800',
error: 'bg-red-100/80 border-red-300 dark:bg-red-900/80 dark:border-red-800',
warning: 'bg-yellow-100/80 border-yellow-300 dark:bg-yellow-900/80 dark:border-yellow-800',
info: 'bg-blue-100/80 border-blue-300 dark:bg-blue-900/80 dark:border-blue-800',
};
const textStyles = {
default: 'text-foreground',
success: 'text-green-900 dark:text-green-50',
error: 'text-red-900 dark:text-red-50',
warning: 'text-yellow-900 dark:text-yellow-50',
info: 'text-blue-900 dark:text-blue-50',
};
const iconColorMap = {
default: colors.mutedForeground,
success: '#22c55e',
error: '#ef4444',
warning: '#eab308',
info: '#3b82f6',
};
const getIcon = () => {
const color = iconColorMap[variant];
switch (variant) {
case 'success': return ;
case 'error': return ;
case 'warning': return ;
case 'info': return ;
default: return null;
}
};
return (
{getIcon()}
{title && {title}}
{description && {description}}
);
}