# Toast

> A succinct message that is displayed temporarily with Dynamic Island animation inspired by iOS.

**BNA UI** — a React Native / Expo component library.
These components render through `react-native`, not the DOM: there are no HTML
elements, no Tailwind classes and no Radix primitives. Source is copied into your
project and imported through `@/components/ui/*`, `@/components/charts/*`,
`@/hooks/*` and `@/theme/*`. Colours come from the `useColor` hook rather than
hardcoded hex; sizing tokens (`HEIGHT`, `FONT_SIZE`, `BORDER_RADIUS`, `CORNERS`)
come from `@/theme/globals`.

- Docs: https://ui.ahmedbna.com/docs/components/toast
- Markdown: https://ui.ahmedbna.com/docs/components/toast.md
- Structured JSON (props, usage, source, examples): https://ui.ahmedbna.com/r/ai/toast.json
- Install payload (source plus every file it imports): https://ui.ahmedbna.com/r/toast.json
- Install: `npx bna-ui add toast`
- npm dependencies: `lucide-react-native`, `react-native-gesture-handler`, `react-native-reanimated`, `react-native-worklets`
- Registry dependencies: `mode-provider`, `useColorScheme`, `colors`, `useColor`, `globals`, `text`
- Preview recording: https://demo.ahmedbna.com/0306-toast-demo.MP4

---

**Example:** A basic toast notification with title and description

```tsx
// components/demo/toast/toast-demo.tsx
import { Button } from '@/components/ui/button';
import { useToast } from '@/components/ui/toast';
import React from 'react';

export function ToastDemo() {
  const { toast } = useToast();

  const showToast = () => {
    toast({
      title: 'Toast Notification',
      description:
        'This is a basic toast notification with title and description.',
      variant: 'default',
    });
  };

  return <Button onPress={showToast}>Show Toast</Button>;
}
```

## Installation

### CLI

```bash
npx bna-ui add toast
```

### Manual

**1.** Install the following dependencies:

```bash
npx expo install react-native-gesture-handler react-native-reanimated lucide-react-native
```

**2.** Copy and paste the following code into your project.

```tsx
// components/ui/toast.tsx
import { Text } from '@/components/ui/text';
import { AlertCircle, Check, Info, X } from 'lucide-react-native';
import React, {
  createContext,
  useCallback,
  useContext,
  useEffect,
  useState,
} from 'react';
import {
  AccessibilityInfo,
  Dimensions,
  Platform,
  TouchableOpacity,
  View,
  ViewStyle,
} from 'react-native';
import {
  Gesture,
  GestureDetector,
  GestureHandlerRootView,
} from 'react-native-gesture-handler';
import Animated, {
  runOnJS,
  useAnimatedStyle,
  useSharedValue,
  withDelay,
  withSpring,
  withTiming,
} from 'react-native-reanimated';

export type ToastVariant = 'default' | 'success' | 'error' | 'warning' | 'info';

export interface ToastData {
  id: string;
  title?: string;
  description?: string;
  variant?: ToastVariant;
  duration?: number;
  action?: {
    label: string;
    onPress: () => void;
  };
}

interface ToastProps extends ToastData {
  onDismiss: (id: string) => void;
  index: number;
}

const { width: screenWidth } = Dimensions.get('window');
const DYNAMIC_ISLAND_HEIGHT = 37;
const EXPANDED_HEIGHT = 85;
const TOAST_MARGIN = 8;
const DYNAMIC_ISLAND_WIDTH = 126;
const EXPANDED_WIDTH = screenWidth - 32;

// Reanimated spring configuration
const SPRING_CONFIG = {
  stiffness: 120,
  damping: 8,
};

export function Toast({
  id,
  title,
  description,
  variant = 'default',
  onDismiss,
  index,
  action,
}: ToastProps) {
  const [isExpanded, setIsExpanded] = useState(false);
  const [reduceMotion, setReduceMotion] = useState(false);

  useEffect(() => {
    AccessibilityInfo.isReduceMotionEnabled().then(setReduceMotion);
    const subscription = AccessibilityInfo.addEventListener(
      'reduceMotionChanged',
      setReduceMotion
    );
    return () => subscription.remove();
  }, []);

  // Reanimated shared values
  const translateY = useSharedValue(-100);
  const translateX = useSharedValue(0);
  const opacity = useSharedValue(0);
  const scale = useSharedValue(0.8);
  const width = useSharedValue(DYNAMIC_ISLAND_WIDTH);
  const height = useSharedValue(DYNAMIC_ISLAND_HEIGHT);
  const borderRadius = useSharedValue(18.5);
  const contentOpacity = useSharedValue(0);

  // Dynamic Island colors (dark theme optimized)
  const backgroundColor = '#1C1C1E'; // iOS Dynamic Island background
  const mutedTextColor = '#8E8E93'; // iOS secondary text color

  useEffect(() => {
    const hasContentToShow = Boolean(title || description || action);

    if (hasContentToShow) {
      // If there's content, start directly with expanded state
      width.value = EXPANDED_WIDTH;
      height.value = EXPANDED_HEIGHT;
      borderRadius.value = 20;
      setIsExpanded(true);

      if (reduceMotion) {
        translateY.value = 0;
        opacity.value = 1;
        scale.value = 1;
        contentOpacity.value = 1;
      } else {
        // Animate in expanded toast
        translateY.value = withSpring(0, SPRING_CONFIG);
        opacity.value = withTiming(1, { duration: 300 });
        scale.value = withSpring(1, SPRING_CONFIG);
        // CORRECTED LINE: Use withDelay to wrap withTiming
        contentOpacity.value = withDelay(100, withTiming(1, { duration: 300 }));
      }
    } else {
      // If no content, show compact Dynamic Island with icon only
      setIsExpanded(false);

      if (reduceMotion) {
        translateY.value = 0;
        opacity.value = 1;
        scale.value = 1;
      } else {
        // Animate in compact toast
        translateY.value = withSpring(0, SPRING_CONFIG);
        opacity.value = withTiming(1, { duration: 200 });
        scale.value = withSpring(1, SPRING_CONFIG);
      }
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [reduceMotion]); // Re-run if the reduced-motion setting resolves after mount

  const getVariantColor = () => {
    switch (variant) {
      case 'success':
        return '#30D158'; // iOS green
      case 'error':
        return '#FF453A'; // iOS red
      case 'warning':
        return '#FF9F0A'; // iOS orange
      case 'info':
        return '#007AFF'; // iOS blue
      default:
        return '#8E8E93'; // iOS gray
    }
  };

  const getIcon = () => {
    const iconProps = { size: 16, color: getVariantColor() };

    switch (variant) {
      case 'success':
        return <Check {...iconProps} />;
      case 'error':
        return <X {...iconProps} />;
      case 'warning':
        return <AlertCircle {...iconProps} />;
      case 'info':
        return <Info {...iconProps} />;
      default:
        return null;
    }
  };

  const dismiss = useCallback(() => {
    if (reduceMotion) {
      onDismiss(id);
      return;
    }

    // This function will be called from the UI thread
    const onDismissAction = () => {
      'worklet';
      runOnJS(onDismiss)(id);
    };

    translateY.value = withSpring(-100, SPRING_CONFIG);
    opacity.value = withTiming(0, { duration: 250 }, (finished) => {
      if (finished) {
        onDismissAction();
      }
    });
    scale.value = withSpring(0.8, SPRING_CONFIG);
  }, [id, onDismiss, reduceMotion]);

  const panGesture = Gesture.Pan()
    .onUpdate((event) => {
      // Reduced motion: swipe-to-dismiss still works (below), it just
      // doesn't visually track the finger.
      if (reduceMotion) return;
      translateX.value = event.translationX;
    })
    .onEnd((event) => {
      const { translationX, velocityX } = event;

      if (
        Math.abs(translationX) > screenWidth * 0.25 ||
        Math.abs(velocityX) > 800
      ) {
        if (reduceMotion) {
          runOnJS(onDismiss)(id);
          return;
        }

        // Dismiss action to be called from the UI thread
        const onDismissAction = () => {
          'worklet';
          runOnJS(onDismiss)(id);
        };

        // Animate out horizontally
        translateX.value = withTiming(
          translationX > 0 ? screenWidth : -screenWidth,
          { duration: 250 }
        );
        opacity.value = withTiming(0, { duration: 250 }, (finished) => {
          if (finished) {
            onDismissAction();
          }
        });
      } else if (!reduceMotion) {
        // Snap back with spring animation
        translateX.value = withSpring(0, SPRING_CONFIG);
      }
    });

  const getTopPosition = () => {
    const statusBarHeight = Platform.OS === 'ios' ? 59 : 20;
    return statusBarHeight + index * (EXPANDED_HEIGHT + TOAST_MARGIN);
  };

  // Animated styles
  const animatedContainerStyle = useAnimatedStyle(() => ({
    opacity: opacity.value,
    transform: [
      { translateY: translateY.value },
      { translateX: translateX.value },
      { scale: scale.value },
    ],
  }));

  const animatedIslandStyle = useAnimatedStyle(() => ({
    width: width.value,
    height: height.value,
    borderRadius: borderRadius.value,
    backgroundColor,
    justifyContent: 'center',
    alignItems: 'center',
    overflow: 'hidden',
  }));

  const animatedContentStyle = useAnimatedStyle(() => ({
    opacity: contentOpacity.value,
  }));

  const toastStyle: ViewStyle = {
    position: 'absolute',
    top: getTopPosition(),
    alignSelf: 'center',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 8 },
    shadowOpacity: 0.25,
    shadowRadius: 20,
    elevation: 10,
    zIndex: 1000 + index,
  };

  return (
    <GestureDetector gesture={panGesture}>
      <Animated.View
        style={[toastStyle, animatedContainerStyle]}
        accessible
        accessibilityRole='alert'
        accessibilityLiveRegion='polite'
        accessibilityLabel={[title, description].filter(Boolean).join('. ')}
      >
        <Animated.View style={animatedIslandStyle}>
          {/* Compact state - just icon or indicator */}
          {!isExpanded && (
            <View style={{ justifyContent: 'center', alignItems: 'center' }}>
              {getIcon()}
            </View>
          )}

          {/* Expanded state - full content */}
          {isExpanded && (
            <Animated.View
              style={[
                {
                  position: 'absolute',
                  top: 0,
                  left: 0,
                  right: 0,
                  bottom: 0,
                  paddingHorizontal: 16,
                  paddingVertical: 12,
                  flexDirection: 'row',
                  alignItems: 'center',
                },
                animatedContentStyle,
              ]}
            >
              {getIcon() && (
                <View style={{ marginRight: 12 }}>{getIcon()}</View>
              )}

              <View style={{ flex: 1, minWidth: 0 }}>
                {title && (
                  <Text
                    variant='subtitle'
                    style={{
                      color: '#FFFFFF',
                      fontSize: 15,
                      fontWeight: '600',
                      marginBottom: description ? 2 : 0,
                    }}
                    numberOfLines={1}
                    ellipsizeMode='tail'
                  >
                    {title}
                  </Text>
                )}
                {description && (
                  <Text
                    variant='caption'
                    style={{
                      color: mutedTextColor,
                      fontSize: 13,
                      fontWeight: '400',
                    }}
                    numberOfLines={2}
                    ellipsizeMode='tail'
                  >
                    {description}
                  </Text>
                )}
              </View>

              {action && (
                <TouchableOpacity
                  onPress={action.onPress}
                  style={{
                    marginLeft: 12,
                    paddingHorizontal: 12,
                    paddingVertical: 6,
                    backgroundColor: getVariantColor(),
                    borderRadius: 12,
                  }}
                >
                  <Text
                    variant='caption'
                    style={{
                      color: '#FFFFFF',
                      fontSize: 12,
                      fontWeight: '600',
                    }}
                  >
                    {action.label}
                  </Text>
                </TouchableOpacity>
              )}

              <TouchableOpacity
                onPress={dismiss}
                style={{ marginLeft: 8, padding: 4, borderRadius: 8 }}
              >
                <X size={14} color={mutedTextColor} />
              </TouchableOpacity>
            </Animated.View>
          )}
        </Animated.View>
      </Animated.View>
    </GestureDetector>
  );
}

interface ToastContextType {
  toast: (toast: Omit<ToastData, 'id'>) => void;
  success: (title: string, description?: string) => void;
  error: (title: string, description?: string) => void;
  warning: (title: string, description?: string) => void;
  info: (title: string, description?: string) => void;
  dismiss: (id: string) => void;
  dismissAll: () => void;
}

const ToastContext = createContext<ToastContextType | null>(null);

interface ToastProviderProps {
  children: React.ReactNode;
  maxToasts?: number;
}

export function ToastProvider({ children, maxToasts = 3 }: ToastProviderProps) {
  const [toasts, setToasts] = useState<ToastData[]>([]);

  const generateId = () => Math.random().toString(36).substr(2, 9);

  const addToast = useCallback(
    (toastData: Omit<ToastData, 'id'>) => {
      const id = generateId();
      const newToast: ToastData = {
        ...toastData,
        id,
        duration: toastData.duration ?? 4000,
      };

      setToasts((prev) => {
        const updated = [newToast, ...prev];
        return updated.slice(0, maxToasts);
      });

      // Auto dismiss after duration
      if (newToast.duration && newToast.duration > 0) {
        setTimeout(() => {
          dismissToast(id);
        }, newToast.duration);
      }
    },
    [maxToasts]
  );

  const dismissToast = useCallback((id: string) => {
    setToasts((prev) => prev.filter((toast) => toast.id !== id));
  }, []);

  const dismissAll = useCallback(() => {
    setToasts([]);
  }, []);

  const createVariantToast = useCallback(
    (variant: ToastVariant, title: string, description?: string) => {
      addToast({
        title,
        description,
        variant,
      });
    },
    [addToast]
  );

  const contextValue: ToastContextType = {
    toast: addToast,
    success: (title, description) =>
      createVariantToast('success', title, description),
    error: (title, description) =>
      createVariantToast('error', title, description),
    warning: (title, description) =>
      createVariantToast('warning', title, description),
    info: (title, description) =>
      createVariantToast('info', title, description),
    dismiss: dismissToast,
    dismissAll,
  };

  const containerStyle: ViewStyle = {
    position: 'absolute',
    top: 0,
    left: 0,
    right: 0,
    zIndex: 1000,
    pointerEvents: 'box-none',
  };

  return (
    <ToastContext.Provider value={contextValue}>
      <GestureHandlerRootView style={{ flex: 1 }}>
        {children}
        <View style={containerStyle} pointerEvents='box-none'>
          {toasts.map((toast, index) => (
            <Toast
              key={toast.id}
              {...toast}
              index={index}
              onDismiss={dismissToast}
            />
          ))}
        </View>
      </GestureHandlerRootView>
    </ToastContext.Provider>
  );
}

// Hook to use toast
export function useToast() {
  const context = useContext(ToastContext);

  if (!context) {
    throw new Error('useToast must be used within a ToastProvider');
  }

  return context;
}
```

**3.** Update the import paths to match your project setup.

**4.** Wrap your app with the ToastProvider.

```tsx
import { ToastProvider } from '@/components/ui/toast';

export default function App() {
  return <ToastProvider>{/* Your app content */}</ToastProvider>;
}
```

## Usage

```tsx
import { useToast } from '@/components/ui/toast';
```

```tsx
function MyComponent() {
  const { toast } = useToast();

  const showToast = () => {
    toast({
      title: 'Success!',
      description: 'Your changes have been saved.',
      variant: 'success',
    });
  };

  return <Button onPress={showToast}>Show Toast</Button>;
}
```

## Examples

#### Default

**Example:** A basic toast notification with title and description

```tsx
// components/demo/toast/toast-demo.tsx
import { Button } from '@/components/ui/button';
import { useToast } from '@/components/ui/toast';
import React from 'react';

export function ToastDemo() {
  const { toast } = useToast();

  const showToast = () => {
    toast({
      title: 'Toast Notification',
      description:
        'This is a basic toast notification with title and description.',
      variant: 'default',
    });
  };

  return <Button onPress={showToast}>Show Toast</Button>;
}
```

#### Variants

**Example:** Toast notifications with different variants (success, error, warning, info)

```tsx
// components/demo/toast/toast-variants.tsx
import { Button } from '@/components/ui/button';
import { useToast } from '@/components/ui/toast';
import { View } from '@/components/ui/view';
import React from 'react';

export function ToastVariants() {
  const { success, error, warning, info } = useToast();

  return (
    <View style={{ gap: 12 }}>
      <Button
        onPress={() =>
          success('Success!', 'Your action was completed successfully.')
        }
        variant='success'
      >
        Success
      </Button>

      <Button
        onPress={() =>
          error('Error!', 'Something went wrong. Please try again.')
        }
        variant='destructive'
      >
        Error
      </Button>

      <Button
        onPress={() =>
          warning('Warning!', 'Please review your input before continuing.')
        }
        variant='secondary'
      >
        Warning
      </Button>

      <Button
        onPress={() => info('Info', "Here's some helpful information for you.")}
      >
        Info
      </Button>
    </View>
  );
}
```

#### With Actions

**Example:** Toast notifications with action buttons

```tsx
// components/demo/toast/toast-actions.tsx
import { Button } from '@/components/ui/button';
import { useToast } from '@/components/ui/toast';
import { View } from '@/components/ui/view';
import React from 'react';

export function ToastActions() {
  const { toast } = useToast();

  const showToastWithAction = () => {
    toast({
      title: 'New message received',
      description: 'You have a new message from John Doe.',
      variant: 'info',
      action: {
        label: 'View',
        onPress: () => {
          console.log('View action pressed');
          // Navigate to message or perform action
        },
      },
    });
  };

  const showUndoToast = () => {
    toast({
      title: 'Item deleted',
      description: 'The item has been removed from your list.',
      variant: 'warning',
      duration: 8000, // Longer duration for undo action
      action: {
        label: 'Undo',
        onPress: () => {
          console.log('Undo action pressed');
          // Restore the deleted item
        },
      },
    });
  };

  return (
    <View style={{ gap: 12 }}>
      <Button onPress={showToastWithAction} variant='outline'>
        Show with Action
      </Button>

      <Button onPress={showUndoToast} variant='outline'>
        Show Undo Toast
      </Button>
    </View>
  );
}
```

#### Custom Duration

**Example:** Toast notifications with custom durations

```tsx
// components/demo/toast/toast-duration.tsx
import { Button } from '@/components/ui/button';
import { useToast } from '@/components/ui/toast';
import { View } from '@/components/ui/view';
import React from 'react';

export function ToastDuration() {
  const { toast } = useToast();

  return (
    <View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 12 }}>
      <Button
        onPress={() =>
          toast({
            title: 'Quick toast',
            description: 'This disappears in 2 seconds',
            duration: 2000,
            variant: 'info',
          })
        }
        variant='outline'
      >
        2 seconds
      </Button>

      <Button
        onPress={() =>
          toast({
            title: 'Standard toast',
            description: 'This disappears in 4 seconds',
            duration: 4000,
            variant: 'default',
          })
        }
        variant='outline'
      >
        4 seconds (default)
      </Button>

      <Button
        onPress={() =>
          toast({
            title: 'Long toast',
            description: 'This disappears in 8 seconds',
            duration: 8000,
            variant: 'warning',
          })
        }
        variant='outline'
      >
        8 seconds
      </Button>

      <Button
        onPress={() =>
          toast({
            title: 'Persistent toast',
            description: "This won't disappear automatically",
            duration: 0, // No auto-dismiss
            variant: 'error',
          })
        }
        variant='outline'
      >
        Persistent
      </Button>
    </View>
  );
}
```

#### Multiple Toasts

**Example:** Multiple toast notifications stacked vertically

```tsx
// components/demo/toast/toast-multiple.tsx
import { Button } from '@/components/ui/button';
import { useToast } from '@/components/ui/toast';
import { View } from '@/components/ui/view';
import React from 'react';

export function ToastMultiple() {
  const { toast, dismissAll } = useToast();

  const showMultipleToasts = () => {
    const variants = ['success', 'warning', 'error', 'info'] as const;
    const messages = [
      { title: 'Success!', description: 'Operation completed successfully' },
      { title: 'Warning', description: 'Please check your input' },
      { title: 'Error', description: 'Something went wrong' },
      { title: 'Info', description: "Here's some information" },
    ];

    variants.forEach((variant, index) => {
      setTimeout(() => {
        toast({
          ...messages[index],
          variant,
          duration: 6000,
        });
      }, index * 500); // Stagger the toasts
    });
  };

  const showBatchToasts = () => {
    // Show multiple toasts at once
    toast({
      title: 'First toast',
      description: 'This is the first toast',
      variant: 'success',
    });

    toast({
      title: 'Second toast',
      description: 'This is the second toast',
      variant: 'info',
    });

    toast({
      title: 'Third toast',
      description: 'This is the third toast',
      variant: 'warning',
    });
  };

  return (
    <View style={{ gap: 12 }}>
      <Button onPress={showMultipleToasts} variant='outline'>
        Show Staggered Toasts
      </Button>

      <Button onPress={showBatchToasts} variant='outline'>
        Show Batch Toasts
      </Button>

      <Button onPress={dismissAll} variant='destructive'>
        Dismiss All Toasts
      </Button>
    </View>
  );
}
```

#### Compact Mode

**Example:** Compact toast notifications without title or description

```tsx
// components/demo/toast/toast-compact.tsx
import { Button } from '@/components/ui/button';
import { useToast } from '@/components/ui/toast';
import { View } from '@/components/ui/view';
import React from 'react';

export function ToastCompact() {
  const { toast } = useToast();

  return (
    <View style={{ gap: 12 }}>
      <Button
        onPress={() =>
          toast({
            variant: 'success',
          })
        }
        variant='success'
      >
        Success Icon Only
      </Button>

      <Button
        onPress={() =>
          toast({
            variant: 'error',
          })
        }
        variant='destructive'
      >
        Error Icon Only
      </Button>

      <Button
        onPress={() =>
          toast({
            variant: 'warning',
          })
        }
        variant='secondary'
      >
        Warning Icon Only
      </Button>

      <Button
        onPress={() =>
          toast({
            variant: 'info',
          })
        }
        variant='outline'
      >
        Info Icon Only
      </Button>

      <Button
        onPress={() =>
          toast({
            title: 'Title only',
          })
        }
      >
        Title Only
      </Button>
    </View>
  );
}
```

## API Reference

### ToastProvider

The provider component that manages toast state and renders toasts.

| Prop        | Type        | Default | Description                                  |
| ----------- | ----------- | ------- | -------------------------------------------- |
| `children`  | `ReactNode` | -       | The app content to wrap with toast provider. |
| `maxToasts` | `number`    | `3`     | Maximum number of toasts to display at once. |

### useToast

Hook that provides methods to show and manage toasts.

```tsx
const { toast, success, error, warning, info, dismiss, dismissAll } =
  useToast();
```

#### Methods

| Method       | Type                                            | Description                     |
| ------------ | ----------------------------------------------- | ------------------------------- |
| `toast`      | `(data: ToastData) => void`                     | Show a toast with custom data.  |
| `success`    | `(title: string, description?: string) => void` | Show a success toast.           |
| `error`      | `(title: string, description?: string) => void` | Show an error toast.            |
| `warning`    | `(title: string, description?: string) => void` | Show a warning toast.           |
| `info`       | `(title: string, description?: string) => void` | Show an info toast.             |
| `dismiss`    | `(id: string) => void`                          | Dismiss a specific toast by ID. |
| `dismissAll` | `() => void`                                    | Dismiss all active toasts.      |

### ToastData

Configuration object for toast notifications.

| Prop          | Type                                                       | Default     | Description                                                                     |
| ------------- | ---------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------- |
| `title`       | `string`                                                   | -           | The title of the toast.                                                         |
| `description` | `string`                                                   | -           | The description text of the toast.                                              |
| `variant`     | `'default' \| 'success' \| 'error' \| 'warning' \| 'info'` | `'default'` | The visual variant of the toast.                                                |
| `duration`    | `number`                                                   | `4000`      | Duration in milliseconds before auto-dismiss. Set to 0 to disable auto-dismiss. |
| `action`      | `{ label: string; onPress: () => void }`                   | -           | Optional action button configuration.                                           |

## Animation

The toast component features iOS Dynamic Island-inspired animations:

- **Entrance**: Smooth slide-in from top with scale and opacity transitions
- **Expansion**: Automatic expansion when content is present
- **Gestures**: Swipe-to-dismiss with spring animations
- **Stacking**: Multiple toasts stack vertically with proper spacing
- **Exit**: Fade out with scale transition

## Accessibility

The Toast component is built with accessibility in mind:

- Each toast exposes `accessibilityRole="alert"` and `accessibilityLiveRegion="polite"` so screen readers announce it as it appears
- The entry/exit spring animation and swipe-to-dismiss gesture are gated behind `AccessibilityInfo.isReduceMotionEnabled()`
- Dismissible with both a swipe gesture and an explicit close button

## Customization

### Custom Colors

You can customize the colors by modifying the `getVariantColor()` function in the component:

```tsx
const getVariantColor = () => {
  switch (variant) {
    case 'success':
      return '#34D399'; // Custom green
    case 'error':
      return '#F87171'; // Custom red
    // ... other variants
  }
};
```

### Custom Positioning

Modify the `getTopPosition()` function to change toast positioning:

```tsx
const getTopPosition = () => {
  const statusBarHeight = Platform.OS === 'ios' ? 59 : 20;
  const customOffset = 20; // Add custom offset
  return (
    statusBarHeight + customOffset + index * (EXPANDED_HEIGHT + TOAST_MARGIN)
  );
};
```

### Custom Animations

The component is built on `react-native-reanimated`'s `withSpring`. You can customize the spring config by editing the `SPRING_CONFIG` constant in `toast.tsx`:

```tsx
const SPRING_CONFIG = {
  stiffness: 120, // Custom stiffness
  damping: 8, // Custom damping
};

translateY.value = withSpring(0, SPRING_CONFIG);
```
