react-native-bread 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,55 +1,91 @@
1
- # 🍞 react-native-bread
1
+ # React Native Bread 🍞
2
2
 
3
- Drop-in toast notifications for React Native. Clean Sonner like API, buttery 60fps animations, swipe-to-dismiss, and fully customizable.
4
-
5
- ```tsx
6
- toast.success('Saved!'); // That's it. No hooks, no context.
7
- ```
3
+ An opinionated toast component for React Native. Inspired by @emilkowalski's Sonner, built for mobile with smooth 60fps animations and intuitive swipe gestures.
8
4
 
5
+ https://github.com/user-attachments/assets/8a862dba-422c-4573-9f12-0a36cf6efe49
9
6
 
7
+ ## Features
10
8
 
11
- https://github.com/user-attachments/assets/8a862dba-422c-4573-9f12-0a36cf6efe49
9
+ - Clean, imperative API inspired by [Sonner](https://sonner.emilkowal.ski/)
10
+ - Zero setup - add one component, start toasting. No hooks, no providers
11
+ - Built for mobile with smooth 60fps animations via Reanimated 3
12
+ - Natural swipe gestures that feel native to the platform
13
+ - Multiple toast types: `success`, `error`, `info`, and `promise`
14
+ - Promise handling with automatic loading → success/error states
15
+ - Toast stacking with configurable limits
16
+ - Position toasts at top or bottom of screen
17
+ - Completely customizable - colors, icons, styles, animations
18
+ - Full Expo compatibility
19
+ - Imperative API works anywhere - components, utilities, event handlers
12
20
 
13
21
 
14
22
 
15
23
  ## Installation
16
24
 
17
- ```bash
25
+ ```sh
18
26
  bun add react-native-bread
19
- # or any package manager
20
27
  ```
21
28
 
22
- ### Peer Dependencies
29
+ #### Requirements
23
30
 
24
- You'll need these installed and configured in your project:
31
+ To use this package, **you also need to install its peer dependencies**. Check out their documentation for more information:
25
32
 
26
- ```bash
27
- bun add react-native-reanimated react-native-gesture-handler react-native-safe-area-context react-native-svg react-native-worklets
28
- ```
33
+ - [React Native Reanimated](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/getting-started)
34
+ - [React Native Gesture Handler](https://docs.swmansion.com/react-native-gesture-handler/docs/)
35
+ - [React Native Safe Area Context](https://docs.expo.dev/versions/latest/sdk/safe-area-context/)
36
+ - [React Native SVG](https://github.com/software-mansion/react-native-svg)
37
+ - [React Native Worklets](https://github.com/margelo/react-native-worklets-core)
29
38
 
30
39
 
31
- ## Quick Start
40
+ ## Usage
32
41
 
33
- Wrap your app with `<BreadLoaf>` and you're good to go:
42
+ ### In your App.tsx/entry point
34
43
 
35
44
  ```tsx
36
45
  import { BreadLoaf } from 'react-native-bread';
37
46
 
38
- export default function App() {
47
+ function App() {
39
48
  return (
40
- <BreadLoaf>
41
- <YourApp />
42
- </BreadLoaf>
49
+ <View>
50
+ <NavigationContainer>...</NavigationContainer>
51
+ <BreadLoaf />
52
+ </View>
43
53
  );
44
54
  }
45
55
  ```
46
56
 
47
- Then show toasts from anywhere:
57
+ ### Expo Router
58
+
59
+ When using Expo Router, place the `BreadLoaf` component in your root layout file (`app/_layout.tsx`):
60
+
61
+ ```tsx
62
+ import { BreadLoaf } from 'react-native-bread';
63
+ import { Stack } from 'expo-router';
64
+
65
+ export default function RootLayout() {
66
+ return (
67
+ <>
68
+ <Stack>
69
+ <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
70
+ <Stack.Screen name="+not-found" />
71
+ </Stack>
72
+ <BreadLoaf />
73
+ </>
74
+ );
75
+ }
76
+ ```
77
+
78
+ This ensures the toasts will be displayed across all screens in your app.
79
+
80
+ ### Show a toast
48
81
 
49
82
  ```tsx
50
83
  import { toast } from 'react-native-bread';
51
84
 
52
- // Basic toasts
85
+ // Basic usage
86
+ toast.success('Saved!');
87
+
88
+ // With description
53
89
  toast.success('Saved!', 'Your changes have been saved');
54
90
  toast.error('Error', 'Something went wrong');
55
91
  toast.info('Tip', 'Swipe to dismiss');
@@ -62,87 +98,68 @@ toast.promise(fetchData(), {
62
98
  });
63
99
  ```
64
100
 
65
- ## API
66
-
67
- ### Toast Methods
68
-
69
- | Method | Description |
70
- |--------|-------------|
71
- | `toast.success(title, description?)` | Green checkmark toast |
72
- | `toast.error(title, description?)` | Red X toast |
73
- | `toast.info(title, description?)` | Yellow info toast |
74
- | `toast.promise(promise, messages)` | Loading → success/error toast |
75
- | `toast.dismiss(id)` | Dismiss a specific toast |
76
- | `toast.dismissAll()` | Dismiss all toasts |
101
+ ## Customization
77
102
 
78
103
  ### Per-Toast Options
79
104
 
80
- Instead of a description string, you can pass an options object as the second argument:
105
+ Pass an options object as the second argument to customize individual toasts:
81
106
 
82
107
  ```tsx
83
108
  toast.success('Saved!', {
84
109
  description: 'Your changes have been saved',
85
110
  duration: 5000,
86
111
  icon: <CustomIcon />,
112
+ style: { backgroundColor: '#fff' },
113
+ dismissible: true,
114
+ showCloseButton: true,
87
115
  });
88
116
  ```
89
117
 
90
- | Option | Type | Description |
91
- |--------|------|-------------|
92
- | `description` | `string` | Toast description text |
93
- | `duration` | `number` | Display time in ms |
94
- | `icon` | `ReactNode \| (props) => ReactNode` | Custom icon component |
95
- | `style` | `ViewStyle` | Toast container style overrides |
96
- | `titleStyle` | `TextStyle` | Title text style overrides |
97
- | `descriptionStyle` | `TextStyle` | Description text style overrides |
98
- | `dismissible` | `boolean` | Enable/disable swipe to dismiss |
99
- | `showCloseButton` | `boolean` | Show/hide the X button |
100
-
101
- ### BreadLoaf Config
118
+ ### Global Configuration
102
119
 
103
- Customize all toasts globally via the `config` prop:
120
+ Customize all toasts globally via the `config` prop on `<BreadLoaf />`:
104
121
 
105
122
  ```tsx
106
- <BreadLoaf config={{ position: 'bottom', stacking: false }}>
107
- <App />
108
- </BreadLoaf>
123
+ <BreadLoaf
124
+ config={{
125
+ position: 'bottom',
126
+ stacking: true,
127
+ maxStack: 3,
128
+ defaultDuration: 4000,
129
+ colors: {
130
+ success: { accent: '#22c55e', background: '#f0fdf4' },
131
+ error: { accent: '#ef4444', background: '#fef2f2' },
132
+ }
133
+ }}
134
+ />
109
135
  ```
110
136
 
111
- | Option | Type | Default | Description |
112
- |--------|------|---------|-------------|
113
- | `position` | `'top' \| 'bottom'` | `'top'` | Where toasts appear |
114
- | `offset` | `number` | `0` | Extra spacing from screen edge (px) |
115
- | `stacking` | `boolean` | `true` | Show multiple toasts stacked |
116
- | `maxStack` | `number` | `3` | Max visible toasts when stacking |
117
- | `dismissible` | `boolean` | `true` | Allow swipe to dismiss |
118
- | `showCloseButton` | `boolean` | `true` | Show X button (except loading toasts) |
119
- | `defaultDuration` | `number` | `4000` | Default display time (ms) |
120
- | `colors` | `object` | — | Colors per toast type (see below) |
121
- | `icons` | `object` | | Custom icons per toast type |
122
- | `toastStyle` | `ViewStyle` | — | Global toast container styles |
123
- | `titleStyle` | `TextStyle` | — | Global title text styles |
124
- | `descriptionStyle` | `TextStyle` | — | Global description text styles |
125
-
126
- #### Colors
127
-
128
- Each toast type (`success`, `error`, `info`, `loading`) accepts:
129
-
130
- | Property | Description |
131
- |----------|-------------|
132
- | `accent` | Icon and title color |
133
- | `background` | Toast background color |
137
+ Available options include:
138
+ - **position**: `'top' | 'bottom'` - Where toasts appear
139
+ - **offset**: Extra spacing from screen edge
140
+ - **stacking**: Show multiple toasts stacked
141
+ - **maxStack**: Max visible toasts when stacking
142
+ - **dismissible**: Allow swipe to dismiss
143
+ - **showCloseButton**: Show X button
144
+ - **defaultDuration**: Default display time in ms
145
+ - **colors**: Custom colors per toast type
146
+ - **icons**: Custom icons per toast type
147
+ - **toastStyle**, **titleStyle**, **descriptionStyle**: Global style overrides
134
148
 
135
- ```tsx
136
- colors: {
137
- success: { accent: '#22c55e', background: '#f0fdf4' },
138
- error: { accent: '#ef4444', background: '#fef2f2' },
139
- }
140
- ```
149
+ ## API Reference
150
+
151
+ | Method | Description |
152
+ |--------|-------------|
153
+ | `toast.success(title, description?)` | Show success toast |
154
+ | `toast.error(title, description?)` | Show error toast |
155
+ | `toast.info(title, description?)` | Show info toast |
156
+ | `toast.promise(promise, messages)` | Show loading → success/error toast |
157
+ | `toast.dismiss(id)` | Dismiss a specific toast |
158
+ | `toast.dismissAll()` | Dismiss all toasts |
141
159
 
142
- ## Known Limitations
160
+ ## Known Issues
143
161
 
144
- ### Toasts Behind Modals
162
+ **Modal Overlays**: Toasts may render behind React Native's `<Modal>` component since modals are mounted at the native layer.
145
163
 
146
- When you trigger a toast while opening a native modal (or transparent modal), the toast may appear **behind** the modal. This happens because React Native modals are mounted natively on top of everything.
164
+ **Solution**: Use absolute positioning within your component tree instead of `<Modal>` for better toast visibility.
147
165
 
148
- **Workaround**: Use a "contained" modal approach — render your modal content inside your regular component tree with absolute positioning, rather than using React Native's `<Modal>` component. This way toasts will appear on top as expected.
@@ -10,17 +10,22 @@ var _toast = require("./toast.js");
10
10
  var _toastStore = require("./toast-store.js");
11
11
  var _jsxRuntime = require("react/jsx-runtime");
12
12
  /**
13
- * Toast provider component that enables toast notifications in your app.
14
- * Wrap your root component with `<BreadLoaf>` to start showing toasts.
13
+ * Toast component that enables toast notifications in your app.
14
+ * Add `<BreadLoaf />` to your root layout to start showing toasts.
15
15
  *
16
16
  * @example
17
17
  * ```tsx
18
18
  * import { BreadLoaf } from 'react-native-bread';
19
19
  *
20
- * // Basic usage
21
- * <BreadLoaf>
22
- * <App />
23
- * </BreadLoaf>
20
+ * // Basic usage - add to your root layout
21
+ * export default function RootLayout() {
22
+ * return (
23
+ * <>
24
+ * <Stack />
25
+ * <BreadLoaf />
26
+ * </>
27
+ * );
28
+ * }
24
29
  *
25
30
  * // With configuration
26
31
  * <BreadLoaf
@@ -34,36 +39,26 @@ var _jsxRuntime = require("react/jsx-runtime");
34
39
  * },
35
40
  * toastStyle: { borderRadius: 12 },
36
41
  * }}
37
- * >
38
- * <App />
39
- * </BreadLoaf>
42
+ * />
40
43
  * ```
41
44
  */
42
45
  function BreadLoaf({
43
- children,
44
46
  config
45
47
  }) {
46
48
  (0, _react.useEffect)(() => {
47
49
  _toastStore.toastStore.setConfig(config);
48
50
  return () => {
49
- // Reset to defaults when this provider unmounts
50
51
  _toastStore.toastStore.setConfig(undefined);
51
52
  };
52
53
  }, [config]);
53
- return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
54
- style: styles.root,
55
- children: [children, /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
56
- style: styles.portalContainer,
57
- pointerEvents: "box-none",
58
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_toast.ToastContainer, {})
59
- })]
54
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
55
+ style: styles.container,
56
+ pointerEvents: "box-none",
57
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_toast.ToastContainer, {})
60
58
  });
61
59
  }
62
60
  const styles = _reactNative.StyleSheet.create({
63
- root: {
64
- flex: 1
65
- },
66
- portalContainer: {
61
+ container: {
67
62
  ..._reactNative.StyleSheet.absoluteFillObject,
68
63
  zIndex: 9999
69
64
  }
@@ -1 +1 @@
1
- {"version":3,"names":["_react","require","_reactNative","_toast","_toastStore","_jsxRuntime","BreadLoaf","children","config","useEffect","toastStore","setConfig","undefined","jsxs","View","style","styles","root","jsx","portalContainer","pointerEvents","ToastContainer","StyleSheet","create","flex","absoluteFillObject","zIndex"],"sourceRoot":"../../src","sources":["toast-provider.tsx"],"mappings":";;;;;;AAAA,IAAAA,MAAA,GAAAC,OAAA;AACA,IAAAC,YAAA,GAAAD,OAAA;AACA,IAAAE,MAAA,GAAAF,OAAA;AACA,IAAAG,WAAA,GAAAH,OAAA;AAA2C,IAAAI,WAAA,GAAAJ,OAAA;AAqB3C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASK,SAASA,CAAC;EAAEC,QAAQ;EAAEC;AAAuB,CAAC,EAAE;EAC9D,IAAAC,gBAAS,EAAC,MAAM;IACdC,sBAAU,CAACC,SAAS,CAACH,MAAM,CAAC;IAC5B,OAAO,MAAM;MACX;MACAE,sBAAU,CAACC,SAAS,CAACC,SAAS,CAAC;IACjC,CAAC;EACH,CAAC,EAAE,CAACJ,MAAM,CAAC,CAAC;EACZ,oBACE,IAAAH,WAAA,CAAAQ,IAAA,EAACX,YAAA,CAAAY,IAAI;IAACC,KAAK,EAAEC,MAAM,CAACC,IAAK;IAAAV,QAAA,GACtBA,QAAQ,eACT,IAAAF,WAAA,CAAAa,GAAA,EAAChB,YAAA,CAAAY,IAAI;MAACC,KAAK,EAAEC,MAAM,CAACG,eAAgB;MAACC,aAAa,EAAC,UAAU;MAAAb,QAAA,eAC3D,IAAAF,WAAA,CAAAa,GAAA,EAACf,MAAA,CAAAkB,cAAc,IAAE;IAAC,CACd,CAAC;EAAA,CACH,CAAC;AAEX;AAEA,MAAML,MAAM,GAAGM,uBAAU,CAACC,MAAM,CAAC;EAC/BN,IAAI,EAAE;IACJO,IAAI,EAAE;EACR,CAAC;EACDL,eAAe,EAAE;IACf,GAAGG,uBAAU,CAACG,kBAAkB;IAChCC,MAAM,EAAE;EACV;AACF,CAAC,CAAC","ignoreList":[]}
1
+ {"version":3,"names":["_react","require","_reactNative","_toast","_toastStore","_jsxRuntime","BreadLoaf","config","useEffect","toastStore","setConfig","undefined","jsx","View","style","styles","container","pointerEvents","children","ToastContainer","StyleSheet","create","absoluteFillObject","zIndex"],"sourceRoot":"../../src","sources":["toast-provider.tsx"],"mappings":";;;;;;AAAA,IAAAA,MAAA,GAAAC,OAAA;AACA,IAAAC,YAAA,GAAAD,OAAA;AACA,IAAAE,MAAA,GAAAF,OAAA;AACA,IAAAG,WAAA,GAAAH,OAAA;AAA2C,IAAAI,WAAA,GAAAJ,OAAA;AAoB3C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASK,SAASA,CAAC;EAAEC;AAAuB,CAAC,EAAE;EACpD,IAAAC,gBAAS,EAAC,MAAM;IACdC,sBAAU,CAACC,SAAS,CAACH,MAAM,CAAC;IAC5B,OAAO,MAAM;MACXE,sBAAU,CAACC,SAAS,CAACC,SAAS,CAAC;IACjC,CAAC;EACH,CAAC,EAAE,CAACJ,MAAM,CAAC,CAAC;EAEZ,oBACE,IAAAF,WAAA,CAAAO,GAAA,EAACV,YAAA,CAAAW,IAAI;IAACC,KAAK,EAAEC,MAAM,CAACC,SAAU;IAACC,aAAa,EAAC,UAAU;IAAAC,QAAA,eACrD,IAAAb,WAAA,CAAAO,GAAA,EAACT,MAAA,CAAAgB,cAAc,IAAE;EAAC,CACd,CAAC;AAEX;AAEA,MAAMJ,MAAM,GAAGK,uBAAU,CAACC,MAAM,CAAC;EAC/BL,SAAS,EAAE;IACT,GAAGI,uBAAU,CAACE,kBAAkB;IAChCC,MAAM,EAAE;EACV;AACF,CAAC,CAAC","ignoreList":[]}
@@ -16,22 +16,45 @@ var _jsxRuntime = require("react/jsx-runtime");
16
16
  function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
17
17
  const ICON_SIZE = 28;
18
18
 
19
- /** Default icon for each toast type */
20
- const DefaultIcon = ({
19
+ /** Memoized default icons to prevent SVG re-renders */
20
+ const MemoizedGreenCheck = /*#__PURE__*/(0, _react.memo)(({
21
+ fill
22
+ }) => /*#__PURE__*/(0, _jsxRuntime.jsx)(_index.GreenCheck, {
23
+ width: 36,
24
+ height: 36,
25
+ fill: fill
26
+ }));
27
+ const MemoizedRedX = /*#__PURE__*/(0, _react.memo)(({
28
+ fill
29
+ }) => /*#__PURE__*/(0, _jsxRuntime.jsx)(_index.RedX, {
30
+ width: ICON_SIZE,
31
+ height: ICON_SIZE,
32
+ fill: fill
33
+ }));
34
+ const MemoizedInfoIcon = /*#__PURE__*/(0, _react.memo)(({
35
+ fill
36
+ }) => /*#__PURE__*/(0, _jsxRuntime.jsx)(_index.InfoIcon, {
37
+ width: ICON_SIZE,
38
+ height: ICON_SIZE,
39
+ fill: fill
40
+ }));
41
+ const MemoizedCloseIcon = /*#__PURE__*/(0, _react.memo)(() => /*#__PURE__*/(0, _jsxRuntime.jsx)(_index.CloseIcon, {
42
+ width: 20,
43
+ height: 20
44
+ }));
45
+
46
+ /** Default icon for each toast type - memoized */
47
+ const DefaultIcon = /*#__PURE__*/(0, _react.memo)(({
21
48
  type,
22
49
  accentColor
23
50
  }) => {
24
51
  switch (type) {
25
52
  case "success":
26
- return /*#__PURE__*/(0, _jsxRuntime.jsx)(_index.GreenCheck, {
27
- width: 36,
28
- height: 36,
53
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(MemoizedGreenCheck, {
29
54
  fill: accentColor
30
55
  });
31
56
  case "error":
32
- return /*#__PURE__*/(0, _jsxRuntime.jsx)(_index.RedX, {
33
- width: ICON_SIZE,
34
- height: ICON_SIZE,
57
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(MemoizedRedX, {
35
58
  fill: accentColor
36
59
  });
37
60
  case "loading":
@@ -40,23 +63,17 @@ const DefaultIcon = ({
40
63
  color: accentColor
41
64
  });
42
65
  case "info":
43
- return /*#__PURE__*/(0, _jsxRuntime.jsx)(_index.InfoIcon, {
44
- width: ICON_SIZE,
45
- height: ICON_SIZE,
66
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(MemoizedInfoIcon, {
46
67
  fill: accentColor
47
68
  });
48
69
  default:
49
- return /*#__PURE__*/(0, _jsxRuntime.jsx)(_index.GreenCheck, {
50
- width: 36,
51
- height: 36,
70
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(MemoizedGreenCheck, {
52
71
  fill: accentColor
53
72
  });
54
73
  }
55
- };
56
-
74
+ });
57
75
  /** Resolves the icon to render - checks per-toast, then config, then default */
58
76
  const resolveIcon = (type, accentColor, customIcon, configIcon) => {
59
- // Per-toast custom icon takes priority
60
77
  if (customIcon) {
61
78
  if (typeof customIcon === "function") {
62
79
  return customIcon({
@@ -66,22 +83,20 @@ const resolveIcon = (type, accentColor, customIcon, configIcon) => {
66
83
  }
67
84
  return customIcon;
68
85
  }
69
-
70
- // Config-level custom icon
71
86
  if (configIcon) {
72
87
  return configIcon({
73
88
  color: accentColor,
74
89
  size: ICON_SIZE
75
90
  });
76
91
  }
77
-
78
- // Default icon
79
92
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(DefaultIcon, {
80
93
  type: type,
81
94
  accentColor: accentColor
82
95
  });
83
96
  };
84
- const AnimatedIcon = ({
97
+
98
+ /** Animated icon wrapper with scale/fade animation */
99
+ const AnimatedIcon = /*#__PURE__*/(0, _react.memo)(({
85
100
  type,
86
101
  accentColor,
87
102
  customIcon,
@@ -104,7 +119,53 @@ const AnimatedIcon = ({
104
119
  style: style,
105
120
  children: resolveIcon(type, accentColor, customIcon, configIcon)
106
121
  });
107
- };
122
+ });
123
+ /** Memoized toast content to prevent inline JSX recreation */
124
+ const ToastContent = /*#__PURE__*/(0, _react.memo)(({
125
+ type,
126
+ title,
127
+ description,
128
+ accentColor,
129
+ customIcon,
130
+ configIcon,
131
+ showCloseButton,
132
+ onDismiss,
133
+ titleStyle,
134
+ descriptionStyle,
135
+ optionsTitleStyle,
136
+ optionsDescriptionStyle
137
+ }) => /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
138
+ style: styles.content,
139
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
140
+ style: styles.icon,
141
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(AnimatedIcon, {
142
+ type: type,
143
+ accentColor: accentColor,
144
+ customIcon: customIcon,
145
+ configIcon: configIcon
146
+ }, type)
147
+ }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
148
+ style: styles.textContainer,
149
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
150
+ maxFontSizeMultiplier: 1.35,
151
+ allowFontScaling: false,
152
+ style: [styles.title, {
153
+ color: accentColor
154
+ }, titleStyle, optionsTitleStyle],
155
+ children: title
156
+ }), description && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
157
+ allowFontScaling: false,
158
+ maxFontSizeMultiplier: 1.35,
159
+ style: [styles.description, descriptionStyle, optionsDescriptionStyle],
160
+ children: description
161
+ })]
162
+ }), showCloseButton && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Pressable, {
163
+ style: styles.closeButton,
164
+ onPress: onDismiss,
165
+ hitSlop: 12,
166
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(MemoizedCloseIcon, {})
167
+ })]
168
+ }));
108
169
 
109
170
  // singleton instance
110
171
  const ToastContainer = () => {
@@ -185,17 +246,10 @@ const ToastItem = ({
185
246
  // Stack position animation
186
247
  const stackIndex = (0, _reactNativeReanimated.useSharedValue)(index);
187
248
 
188
- // Title color animation on variant change
189
- const colorProgress = (0, _reactNativeReanimated.useSharedValue)(1);
190
- const fromColor = (0, _reactNativeReanimated.useSharedValue)(theme.colors[toast.type].accent);
191
- const toColor = (0, _reactNativeReanimated.useSharedValue)(theme.colors[toast.type].accent);
192
-
193
249
  // Refs for tracking previous values to avoid unnecessary animations
194
250
  const lastHandledType = (0, _react.useRef)(toast.type);
195
251
  const prevIndex = (0, _react.useRef)(index);
196
252
  const hasEntered = (0, _react.useRef)(false);
197
-
198
- // Combined animation effect for entry, exit, color transitions, and stack position
199
253
  (0, _react.useEffect)(() => {
200
254
  // Entry animation (only once on mount)
201
255
  if (!hasEntered.current && !toast.isExiting) {
@@ -218,16 +272,9 @@ const ToastItem = ({
218
272
  });
219
273
  }
220
274
 
221
- // Color transition when type changes
275
+ // Track type changes (for icon animation via key)
222
276
  if (toast.type !== lastHandledType.current) {
223
- fromColor.value = theme.colors[lastHandledType.current].accent;
224
- toColor.value = theme.colors[toast.type].accent;
225
277
  lastHandledType.current = toast.type;
226
- colorProgress.value = 0;
227
- colorProgress.value = (0, _reactNativeReanimated.withTiming)(1, {
228
- duration: 300,
229
- easing: EASING
230
- });
231
278
  }
232
279
 
233
280
  // Stack position animation when index changes
@@ -238,14 +285,11 @@ const ToastItem = ({
238
285
  });
239
286
  prevIndex.current = index;
240
287
  }
241
- }, [toast.isExiting, toast.type, index, progress, translationY, fromColor, toColor, colorProgress, stackIndex, exitToY, theme.colors]);
242
- const titleColorStyle = (0, _reactNativeReanimated.useAnimatedStyle)(() => ({
243
- color: (0, _reactNativeReanimated.interpolateColor)(colorProgress.value, [0, 1], [fromColor.value, toColor.value])
244
- }));
288
+ }, [toast.isExiting, toast.type, index, progress, translationY, stackIndex, exitToY]);
245
289
  const dismissToast = (0, _react.useCallback)(() => {
246
290
  _toastStore.toastStore.hide(toast.id);
247
291
  }, [toast.id]);
248
- const panGesture = _reactNativeGestureHandler.Gesture.Pan().onStart(() => {
292
+ const panGesture = (0, _react.useMemo)(() => _reactNativeGestureHandler.Gesture.Pan().onStart(() => {
249
293
  "worklet";
250
294
 
251
295
  isBeingDragged.value = true;
@@ -292,7 +336,13 @@ const ToastItem = ({
292
336
  easing: EASING
293
337
  });
294
338
  }
295
- });
339
+ }), [isBottom, dismissToast, progress, translationY, shouldDismiss, isBeingDragged]);
340
+
341
+ // Memoize disabled gesture to avoid recreation on every render
342
+ const disabledGesture = (0, _react.useMemo)(() => _reactNativeGestureHandler.Gesture.Pan().enabled(false), []);
343
+
344
+ // Derive zIndex separately - it's not animatable and shouldn't trigger worklet re-runs
345
+ const zIndex = (0, _reactNativeReanimated.useDerivedValue)(() => 1000 - Math.round(stackIndex.value));
296
346
  const animatedStyle = (0, _reactNativeReanimated.useAnimatedStyle)(() => {
297
347
  const baseTranslateY = (0, _reactNativeReanimated.interpolate)(progress.value, [0, 1], [entryFromY, ToY]);
298
348
 
@@ -316,7 +366,7 @@ const ToastItem = ({
316
366
  scale
317
367
  }],
318
368
  opacity,
319
- zIndex: 1000 - stackIndex.value
369
+ zIndex: zIndex.value
320
370
  };
321
371
  });
322
372
  const accentColor = theme.colors[toast.type].accent;
@@ -339,50 +389,36 @@ const ToastItem = ({
339
389
  const shouldShowCloseButton = toast.type !== "loading" && (options?.showCloseButton ?? theme.showCloseButton);
340
390
 
341
391
  // Enable/disable gesture based on dismissible setting
342
- const gesture = isDismissible ? panGesture : _reactNativeGestureHandler.Gesture.Pan().enabled(false);
392
+ const gesture = isDismissible ? panGesture : disabledGesture;
393
+ const animStyle = [styles.toast, verticalAnchor, {
394
+ backgroundColor
395
+ }, theme.toastStyle, options?.style, animatedStyle];
343
396
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeGestureHandler.GestureDetector, {
344
397
  gesture: gesture,
345
398
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeReanimated.default.View, {
346
- style: [styles.toast, verticalAnchor, {
347
- backgroundColor
348
- }, theme.toastStyle, options?.style, animatedStyle],
349
- children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
350
- style: styles.content,
351
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
352
- style: styles.icon,
353
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(AnimatedIcon, {
354
- type: toast.type,
355
- accentColor: accentColor,
356
- customIcon: customIcon,
357
- configIcon: configIcon
358
- }, toast.type)
359
- }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
360
- style: styles.textContainer,
361
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeReanimated.default.Text, {
362
- maxFontSizeMultiplier: 1.35,
363
- allowFontScaling: false,
364
- style: [styles.title, theme.titleStyle, options?.titleStyle, titleColorStyle],
365
- children: toast.title
366
- }), toast.description && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
367
- allowFontScaling: false,
368
- maxFontSizeMultiplier: 1.35,
369
- style: [styles.description, theme.descriptionStyle, options?.descriptionStyle],
370
- children: toast.description
371
- })]
372
- }), shouldShowCloseButton && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Pressable, {
373
- style: styles.closeButton,
374
- onPress: dismissToast,
375
- hitSlop: 12,
376
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_index.CloseIcon, {
377
- width: 20,
378
- height: 20
379
- })
380
- })]
399
+ style: animStyle,
400
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(ToastContent, {
401
+ type: toast.type,
402
+ title: toast.title,
403
+ description: toast.description,
404
+ accentColor: accentColor,
405
+ customIcon: customIcon,
406
+ configIcon: configIcon,
407
+ showCloseButton: shouldShowCloseButton,
408
+ onDismiss: dismissToast,
409
+ titleStyle: theme.titleStyle,
410
+ descriptionStyle: theme.descriptionStyle,
411
+ optionsTitleStyle: options?.titleStyle,
412
+ optionsDescriptionStyle: options?.descriptionStyle
381
413
  })
382
414
  })
383
415
  });
384
416
  };
385
- const MemoizedToastItem = /*#__PURE__*/(0, _react.memo)(ToastItem);
417
+
418
+ // Custom comparison to prevent re-renders when toast object reference changes but content is same
419
+ const MemoizedToastItem = /*#__PURE__*/(0, _react.memo)(ToastItem, (prev, next) => {
420
+ return prev.toast.id === next.toast.id && prev.toast.type === next.toast.type && prev.toast.title === next.toast.title && prev.toast.description === next.toast.description && prev.toast.isExiting === next.toast.isExiting && prev.index === next.index && prev.position === next.position && prev.theme === next.theme;
421
+ });
386
422
  const styles = _reactNative.StyleSheet.create({
387
423
  container: {
388
424
  position: "absolute",