@cdx-ui/components 0.0.1-beta.79 → 0.0.1-beta.80

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.
Files changed (214) hide show
  1. package/lib/commonjs/animation/hooks/index.js +17 -0
  2. package/lib/commonjs/animation/hooks/index.js.map +1 -0
  3. package/lib/commonjs/animation/hooks/useCombinedAnimationDisabledState.js +50 -0
  4. package/lib/commonjs/animation/hooks/useCombinedAnimationDisabledState.js.map +1 -0
  5. package/lib/commonjs/animation/index.js +39 -0
  6. package/lib/commonjs/animation/index.js.map +1 -0
  7. package/lib/commonjs/animation/types.js +6 -0
  8. package/lib/commonjs/animation/types.js.map +1 -0
  9. package/lib/commonjs/animation/utils.js +223 -0
  10. package/lib/commonjs/animation/utils.js.map +1 -0
  11. package/lib/commonjs/components/CLAUDE.md +1 -1
  12. package/lib/commonjs/components/Card/index.js +45 -17
  13. package/lib/commonjs/components/Card/index.js.map +1 -1
  14. package/lib/commonjs/components/Toast/animation.js +329 -0
  15. package/lib/commonjs/components/Toast/animation.js.map +1 -0
  16. package/lib/commonjs/components/Toast/hooks.js +66 -0
  17. package/lib/commonjs/components/Toast/hooks.js.map +1 -0
  18. package/lib/commonjs/components/Toast/index.js +493 -0
  19. package/lib/commonjs/components/Toast/index.js.map +1 -0
  20. package/lib/commonjs/components/Toast/styles.js +141 -0
  21. package/lib/commonjs/components/Toast/styles.js.map +1 -0
  22. package/lib/commonjs/components/Toast/types.js +6 -0
  23. package/lib/commonjs/components/Toast/types.js.map +1 -0
  24. package/lib/commonjs/components/index.js +12 -0
  25. package/lib/commonjs/components/index.js.map +1 -1
  26. package/lib/commonjs/index.js +15 -0
  27. package/lib/commonjs/index.js.map +1 -1
  28. package/lib/commonjs/providers/animation-settings/index.js +31 -0
  29. package/lib/commonjs/providers/animation-settings/index.js.map +1 -0
  30. package/lib/commonjs/providers/animation-settings/provider.js +59 -0
  31. package/lib/commonjs/providers/animation-settings/provider.js.map +1 -0
  32. package/lib/commonjs/providers/animation-settings/types.js +6 -0
  33. package/lib/commonjs/providers/animation-settings/types.js.map +1 -0
  34. package/lib/commonjs/providers/forge-design/index.js +29 -0
  35. package/lib/commonjs/providers/forge-design/index.js.map +1 -0
  36. package/lib/commonjs/providers/forge-design/provider.js +71 -0
  37. package/lib/commonjs/providers/forge-design/provider.js.map +1 -0
  38. package/lib/commonjs/providers/forge-design/types.js +6 -0
  39. package/lib/commonjs/providers/forge-design/types.js.map +1 -0
  40. package/lib/commonjs/providers/text-component/index.js +28 -0
  41. package/lib/commonjs/providers/text-component/index.js.map +1 -0
  42. package/lib/commonjs/providers/text-component/provider.js +11 -0
  43. package/lib/commonjs/providers/text-component/provider.js.map +1 -0
  44. package/lib/commonjs/providers/text-component/types.js +6 -0
  45. package/lib/commonjs/providers/text-component/types.js.map +1 -0
  46. package/lib/commonjs/providers/toast/index.js +33 -0
  47. package/lib/commonjs/providers/toast/index.js.map +1 -0
  48. package/lib/commonjs/providers/toast/insets-container.js +160 -0
  49. package/lib/commonjs/providers/toast/insets-container.js.map +1 -0
  50. package/lib/commonjs/providers/toast/lifecycle.context.js +36 -0
  51. package/lib/commonjs/providers/toast/lifecycle.context.js.map +1 -0
  52. package/lib/commonjs/providers/toast/provider.js +508 -0
  53. package/lib/commonjs/providers/toast/provider.js.map +1 -0
  54. package/lib/commonjs/providers/toast/reducer.js +46 -0
  55. package/lib/commonjs/providers/toast/reducer.js.map +1 -0
  56. package/lib/commonjs/providers/toast/toast-config.context.js +29 -0
  57. package/lib/commonjs/providers/toast/toast-config.context.js.map +1 -0
  58. package/lib/commonjs/providers/toast/toast-item-renderer.js +43 -0
  59. package/lib/commonjs/providers/toast/toast-item-renderer.js.map +1 -0
  60. package/lib/commonjs/providers/toast/types.js +6 -0
  61. package/lib/commonjs/providers/toast/types.js.map +1 -0
  62. package/lib/module/animation/hooks/index.js +4 -0
  63. package/lib/module/animation/hooks/index.js.map +1 -0
  64. package/lib/module/animation/hooks/useCombinedAnimationDisabledState.js +47 -0
  65. package/lib/module/animation/hooks/useCombinedAnimationDisabledState.js.map +1 -0
  66. package/lib/module/animation/index.js +6 -0
  67. package/lib/module/animation/index.js.map +1 -0
  68. package/lib/module/animation/types.js +4 -0
  69. package/lib/module/animation/types.js.map +1 -0
  70. package/lib/module/animation/utils.js +212 -0
  71. package/lib/module/animation/utils.js.map +1 -0
  72. package/lib/module/components/CLAUDE.md +1 -1
  73. package/lib/module/components/Card/index.js +47 -19
  74. package/lib/module/components/Card/index.js.map +1 -1
  75. package/lib/module/components/Toast/animation.js +324 -0
  76. package/lib/module/components/Toast/animation.js.map +1 -0
  77. package/lib/module/components/Toast/hooks.js +61 -0
  78. package/lib/module/components/Toast/hooks.js.map +1 -0
  79. package/lib/module/components/Toast/index.js +487 -0
  80. package/lib/module/components/Toast/index.js.map +1 -0
  81. package/lib/module/components/Toast/styles.js +138 -0
  82. package/lib/module/components/Toast/styles.js.map +1 -0
  83. package/lib/module/components/Toast/types.js +4 -0
  84. package/lib/module/components/Toast/types.js.map +1 -0
  85. package/lib/module/components/index.js +1 -0
  86. package/lib/module/components/index.js.map +1 -1
  87. package/lib/module/index.js +1 -0
  88. package/lib/module/index.js.map +1 -1
  89. package/lib/module/providers/animation-settings/index.js +4 -0
  90. package/lib/module/providers/animation-settings/index.js.map +1 -0
  91. package/lib/module/providers/animation-settings/provider.js +52 -0
  92. package/lib/module/providers/animation-settings/provider.js.map +1 -0
  93. package/lib/module/providers/animation-settings/types.js +4 -0
  94. package/lib/module/providers/animation-settings/types.js.map +1 -0
  95. package/lib/module/providers/forge-design/index.js +5 -0
  96. package/lib/module/providers/forge-design/index.js.map +1 -0
  97. package/lib/module/providers/forge-design/provider.js +66 -0
  98. package/lib/module/providers/forge-design/provider.js.map +1 -0
  99. package/lib/module/providers/forge-design/types.js +4 -0
  100. package/lib/module/providers/forge-design/types.js.map +1 -0
  101. package/lib/module/providers/text-component/index.js +5 -0
  102. package/lib/module/providers/text-component/index.js.map +1 -0
  103. package/lib/module/providers/text-component/provider.js +6 -0
  104. package/lib/module/providers/text-component/provider.js.map +1 -0
  105. package/lib/module/providers/text-component/types.js +4 -0
  106. package/lib/module/providers/text-component/types.js.map +1 -0
  107. package/lib/module/providers/toast/index.js +6 -0
  108. package/lib/module/providers/toast/index.js.map +1 -0
  109. package/lib/module/providers/toast/insets-container.js +156 -0
  110. package/lib/module/providers/toast/insets-container.js.map +1 -0
  111. package/lib/module/providers/toast/lifecycle.context.js +32 -0
  112. package/lib/module/providers/toast/lifecycle.context.js.map +1 -0
  113. package/lib/module/providers/toast/provider.js +503 -0
  114. package/lib/module/providers/toast/provider.js.map +1 -0
  115. package/lib/module/providers/toast/reducer.js +42 -0
  116. package/lib/module/providers/toast/reducer.js.map +1 -0
  117. package/lib/module/providers/toast/toast-config.context.js +25 -0
  118. package/lib/module/providers/toast/toast-config.context.js.map +1 -0
  119. package/lib/module/providers/toast/toast-item-renderer.js +39 -0
  120. package/lib/module/providers/toast/toast-item-renderer.js.map +1 -0
  121. package/lib/module/providers/toast/types.js +4 -0
  122. package/lib/module/providers/toast/types.js.map +1 -0
  123. package/lib/typescript/animation/hooks/index.d.ts +2 -0
  124. package/lib/typescript/animation/hooks/index.d.ts.map +1 -0
  125. package/lib/typescript/animation/hooks/useCombinedAnimationDisabledState.d.ts +22 -0
  126. package/lib/typescript/animation/hooks/useCombinedAnimationDisabledState.d.ts.map +1 -0
  127. package/lib/typescript/animation/index.d.ts +4 -0
  128. package/lib/typescript/animation/index.d.ts.map +1 -0
  129. package/lib/typescript/animation/types.d.ts +105 -0
  130. package/lib/typescript/animation/types.d.ts.map +1 -0
  131. package/lib/typescript/animation/utils.d.ts +120 -0
  132. package/lib/typescript/animation/utils.d.ts.map +1 -0
  133. package/lib/typescript/components/Card/index.d.ts +4 -0
  134. package/lib/typescript/components/Card/index.d.ts.map +1 -1
  135. package/lib/typescript/components/Toast/animation.d.ts +21 -0
  136. package/lib/typescript/components/Toast/animation.d.ts.map +1 -0
  137. package/lib/typescript/components/Toast/hooks.d.ts +17 -0
  138. package/lib/typescript/components/Toast/hooks.d.ts.map +1 -0
  139. package/lib/typescript/components/Toast/index.d.ts +28 -0
  140. package/lib/typescript/components/Toast/index.d.ts.map +1 -0
  141. package/lib/typescript/components/Toast/styles.d.ts +73 -0
  142. package/lib/typescript/components/Toast/styles.d.ts.map +1 -0
  143. package/lib/typescript/components/Toast/types.d.ts +303 -0
  144. package/lib/typescript/components/Toast/types.d.ts.map +1 -0
  145. package/lib/typescript/components/index.d.ts +1 -0
  146. package/lib/typescript/components/index.d.ts.map +1 -1
  147. package/lib/typescript/index.d.ts +2 -0
  148. package/lib/typescript/index.d.ts.map +1 -1
  149. package/lib/typescript/providers/animation-settings/index.d.ts +3 -0
  150. package/lib/typescript/providers/animation-settings/index.d.ts.map +1 -0
  151. package/lib/typescript/providers/animation-settings/provider.d.ts +28 -0
  152. package/lib/typescript/providers/animation-settings/provider.d.ts.map +1 -0
  153. package/lib/typescript/providers/animation-settings/types.d.ts +35 -0
  154. package/lib/typescript/providers/animation-settings/types.d.ts.map +1 -0
  155. package/lib/typescript/providers/forge-design/index.d.ts +3 -0
  156. package/lib/typescript/providers/forge-design/index.d.ts.map +1 -0
  157. package/lib/typescript/providers/forge-design/provider.d.ts +24 -0
  158. package/lib/typescript/providers/forge-design/provider.d.ts.map +1 -0
  159. package/lib/typescript/providers/forge-design/types.d.ts +82 -0
  160. package/lib/typescript/providers/forge-design/types.d.ts.map +1 -0
  161. package/lib/typescript/providers/text-component/index.d.ts +3 -0
  162. package/lib/typescript/providers/text-component/index.d.ts.map +1 -0
  163. package/lib/typescript/providers/text-component/provider.d.ts +7 -0
  164. package/lib/typescript/providers/text-component/provider.d.ts.map +1 -0
  165. package/lib/typescript/providers/text-component/types.d.ts +51 -0
  166. package/lib/typescript/providers/text-component/types.d.ts.map +1 -0
  167. package/lib/typescript/providers/toast/index.d.ts +5 -0
  168. package/lib/typescript/providers/toast/index.d.ts.map +1 -0
  169. package/lib/typescript/providers/toast/insets-container.d.ts +72 -0
  170. package/lib/typescript/providers/toast/insets-container.d.ts.map +1 -0
  171. package/lib/typescript/providers/toast/lifecycle.context.d.ts +32 -0
  172. package/lib/typescript/providers/toast/lifecycle.context.d.ts.map +1 -0
  173. package/lib/typescript/providers/toast/provider.d.ts +32 -0
  174. package/lib/typescript/providers/toast/provider.d.ts.map +1 -0
  175. package/lib/typescript/providers/toast/reducer.d.ts +14 -0
  176. package/lib/typescript/providers/toast/reducer.d.ts.map +1 -0
  177. package/lib/typescript/providers/toast/toast-config.context.d.ts +20 -0
  178. package/lib/typescript/providers/toast/toast-config.context.d.ts.map +1 -0
  179. package/lib/typescript/providers/toast/toast-item-renderer.d.ts +7 -0
  180. package/lib/typescript/providers/toast/toast-item-renderer.d.ts.map +1 -0
  181. package/lib/typescript/providers/toast/types.d.ts +422 -0
  182. package/lib/typescript/providers/toast/types.d.ts.map +1 -0
  183. package/package.json +4 -4
  184. package/src/animation/hooks/index.ts +1 -0
  185. package/src/animation/hooks/useCombinedAnimationDisabledState.ts +46 -0
  186. package/src/animation/index.ts +3 -0
  187. package/src/animation/types.ts +128 -0
  188. package/src/animation/utils.ts +249 -0
  189. package/src/components/CLAUDE.md +1 -1
  190. package/src/components/Card/index.tsx +58 -28
  191. package/src/components/Toast/animation.ts +387 -0
  192. package/src/components/Toast/hooks.ts +92 -0
  193. package/src/components/Toast/index.tsx +520 -0
  194. package/src/components/Toast/styles.ts +186 -0
  195. package/src/components/Toast/types.ts +332 -0
  196. package/src/components/index.ts +1 -0
  197. package/src/index.ts +2 -0
  198. package/src/providers/animation-settings/index.ts +11 -0
  199. package/src/providers/animation-settings/provider.tsx +54 -0
  200. package/src/providers/animation-settings/types.ts +37 -0
  201. package/src/providers/forge-design/index.ts +2 -0
  202. package/src/providers/forge-design/provider.tsx +61 -0
  203. package/src/providers/forge-design/types.ts +84 -0
  204. package/src/providers/text-component/index.ts +2 -0
  205. package/src/providers/text-component/provider.tsx +7 -0
  206. package/src/providers/text-component/types.ts +52 -0
  207. package/src/providers/toast/index.ts +4 -0
  208. package/src/providers/toast/insets-container.tsx +230 -0
  209. package/src/providers/toast/lifecycle.context.tsx +37 -0
  210. package/src/providers/toast/provider.tsx +567 -0
  211. package/src/providers/toast/reducer.ts +40 -0
  212. package/src/providers/toast/toast-config.context.ts +25 -0
  213. package/src/providers/toast/toast-item-renderer.tsx +40 -0
  214. package/src/providers/toast/types.ts +431 -0
@@ -0,0 +1,567 @@
1
+ import {
2
+ createContext,
3
+ useCallback,
4
+ useContext,
5
+ useEffect,
6
+ useMemo,
7
+ useReducer,
8
+ useRef,
9
+ } from 'react';
10
+ import { Platform, View } from 'react-native';
11
+ import { useSharedValue } from 'react-native-reanimated';
12
+ import { DefaultToast } from '../../components/Toast';
13
+ import { InsetsContainer } from './insets-container';
14
+ import { ToastLifecycleContext, type ToastLifecycleContextValue } from './lifecycle.context';
15
+ import { toastReducer } from './reducer';
16
+ import { ToastConfigContext } from './toast-config.context';
17
+ import { ToastItemRenderer } from './toast-item-renderer';
18
+ import type {
19
+ ToastComponentProps,
20
+ ToasterContextValue,
21
+ ToastGlobalConfig,
22
+ ToastProviderProps,
23
+ ToastShowConfig,
24
+ ToastShowOptions,
25
+ ToastShowOptionsWithComponent,
26
+ } from './types';
27
+
28
+ const DEFAULT_DURATION = 4000;
29
+
30
+ /**
31
+ * Context for toast manager
32
+ */
33
+ const ToasterContext = createContext<ToasterContextValue | null>(null);
34
+
35
+ /**
36
+ * Merges global config with local config, ensuring local config takes precedence
37
+ * Only includes defined values from localConfig to avoid overriding global config with undefined
38
+ */
39
+ function mergeToastConfig(
40
+ globalConfig: ToastGlobalConfig | undefined,
41
+ localConfig: Partial<ToastGlobalConfig>,
42
+ ): Partial<ToastGlobalConfig> {
43
+ const result: Partial<ToastGlobalConfig> = { ...globalConfig };
44
+
45
+ // Only override with defined values from localConfig
46
+ if (localConfig.color !== undefined) {
47
+ result.color = localConfig.color;
48
+ }
49
+ if (localConfig.placement !== undefined) {
50
+ result.placement = localConfig.placement;
51
+ }
52
+ if (localConfig.isSwipeable !== undefined) {
53
+ result.isSwipeable = localConfig.isSwipeable;
54
+ }
55
+ if (localConfig.animation !== undefined) {
56
+ result.animation = localConfig.animation;
57
+ }
58
+
59
+ return result;
60
+ }
61
+
62
+ /**
63
+ * Creates a component function for simple string toast
64
+ */
65
+ function createStringToastComponent(
66
+ label: string,
67
+ globalConfig: ToastGlobalConfig | undefined,
68
+ ): (props: ToastComponentProps) => React.ReactElement {
69
+ return (props: ToastComponentProps) => {
70
+ const mergedConfig = mergeToastConfig(globalConfig, {
71
+ color: 'default',
72
+ });
73
+ return (
74
+ <DefaultToast
75
+ {...props}
76
+ label={label}
77
+ color={mergedConfig.color}
78
+ placement={mergedConfig.placement}
79
+ isSwipeable={mergedConfig.isSwipeable}
80
+ animation={mergedConfig.animation}
81
+ />
82
+ );
83
+ };
84
+ }
85
+
86
+ /**
87
+ * Creates a component function for config-based toast
88
+ */
89
+ function createConfigToastComponent(
90
+ config: ToastShowConfig,
91
+ globalConfig: ToastGlobalConfig | undefined,
92
+ ): (props: ToastComponentProps) => React.ReactElement {
93
+ return (props: ToastComponentProps) => {
94
+ const mergedConfig = mergeToastConfig(globalConfig, {
95
+ color: config.color,
96
+ placement: config.placement,
97
+ isSwipeable: config.isSwipeable,
98
+ animation: config.animation,
99
+ });
100
+ return (
101
+ <DefaultToast
102
+ {...props}
103
+ color={mergedConfig.color}
104
+ placement={mergedConfig.placement}
105
+ isSwipeable={mergedConfig.isSwipeable}
106
+ animation={mergedConfig.animation}
107
+ label={config.label}
108
+ description={config.description}
109
+ actionLabel={config.actionLabel}
110
+ onActionPress={config.onActionPress}
111
+ icon={config.icon}
112
+ />
113
+ );
114
+ };
115
+ }
116
+
117
+ /**
118
+ * Toast provider component
119
+ * Wraps your app to enable toast functionality
120
+ */
121
+ export function ToastProvider({
122
+ defaultProps,
123
+ insets,
124
+ maxVisibleToasts = 3,
125
+ contentWrapper,
126
+ children,
127
+ disableFullWindowOverlay = false,
128
+ unstable_accessibilityContainerViewIsModal,
129
+ announceLabel,
130
+ }: Readonly<ToastProviderProps>) {
131
+ const [toasts, dispatch] = useReducer(toastReducer, []);
132
+
133
+ /**
134
+ * Memoize global config to prevent unnecessary re-renders
135
+ */
136
+ const globalConfig = useMemo<ToastGlobalConfig | undefined>(() => defaultProps, [defaultProps]);
137
+
138
+ const isToastVisible = toasts.length > 0;
139
+
140
+ const heights = useSharedValue<Record<string, number>>({});
141
+
142
+ const total = useSharedValue<number>(0);
143
+
144
+ /**
145
+ * Derive total from toasts.length so the animated opacity/scale/translateY
146
+ * interpolations always use the real count. Manual increment/decrement
147
+ * was prone to drift when hide + show ran in the same tick or when
148
+ * auto-dismiss raced with a manual hide (stale-closure mismatch).
149
+ */
150
+ useEffect(() => {
151
+ total.set(toasts.length);
152
+ }, [toasts.length, total]);
153
+
154
+ const idCounter = useRef(0);
155
+
156
+ /**
157
+ * Timer state per toast — tracks remaining time so pause/resume can
158
+ * restart with only the leftover duration (WCAG 2.2.1).
159
+ */
160
+ const timersRef = useRef<
161
+ Map<
162
+ string,
163
+ { startedAt: number; remaining: number; timer: NodeJS.Timeout | null; duration: number }
164
+ >
165
+ >(new Map());
166
+ const pausedRef = useRef(false);
167
+ const hideRef = useRef<((ids?: (string & {}) | string[] | 'all') => void) | null>(null);
168
+
169
+ /**
170
+ * Safety reset: when all toasts are gone the InsetsContainer unmounts and
171
+ * no resume event can fire. Clear the paused flag and any orphaned timer
172
+ * entries so the next toast's timer starts normally.
173
+ */
174
+ useEffect(() => {
175
+ if (toasts.length === 0) {
176
+ pausedRef.current = false;
177
+ timersRef.current.forEach((entry) => {
178
+ if (entry.timer) clearTimeout(entry.timer);
179
+ });
180
+ timersRef.current.clear();
181
+ }
182
+ }, [toasts.length]);
183
+
184
+ /**
185
+ * Remove one or more toasts from the array. On web this is the second half
186
+ * of the two-phase hide flow — the toast component calls it from
187
+ * `onTransitionEnd` (or a safety timeout) after the exit transition plays.
188
+ * Also cleans up per-id height state.
189
+ */
190
+ const remove = useCallback(
191
+ (ids: string | string[]) => {
192
+ const idsArray = Array.isArray(ids) ? ids : [ids];
193
+ if (idsArray.length === 0) return;
194
+
195
+ dispatch({ type: 'REMOVE', payload: { ids: idsArray } });
196
+
197
+ heights.modify(<T extends Record<string, number>>(value: T): T => {
198
+ 'worklet';
199
+ const result = { ...value };
200
+ for (const id of idsArray) {
201
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
202
+ delete result[id];
203
+ }
204
+ return result;
205
+ });
206
+ },
207
+ [heights],
208
+ );
209
+
210
+ /**
211
+ * Hide one or more toasts
212
+ * - No argument: hides the last toast in the array
213
+ * - "all": hides all toasts
214
+ * - Single ID: hides that toast
215
+ * - Array of IDs: hides those toasts
216
+ *
217
+ * On web this only marks toasts as `pendingRemoval` so their CSS exit
218
+ * transition can play; the actual removal happens when the toast
219
+ * component calls `remove(id)` on transition end.
220
+ * On native this dispatches removal directly — Reanimated's `exiting`
221
+ * layout animation plays before unmount.
222
+ */
223
+ const hide = useCallback(
224
+ (ids?: (string & {}) | string[] | 'all') => {
225
+ const isWeb = Platform.OS === 'web';
226
+
227
+ if (ids === undefined) {
228
+ if (toasts.length === 0) return;
229
+ // Hide the last non-pending toast in the array. Using the last
230
+ // pending toast would be a no-op on web.
231
+ let lastToast = null;
232
+ for (let i = toasts.length - 1; i >= 0; i--) {
233
+ const candidate = toasts[i];
234
+ if (!candidate.pendingRemoval) {
235
+ lastToast = candidate;
236
+ break;
237
+ }
238
+ }
239
+ if (!lastToast) return;
240
+
241
+ const timerEntry = timersRef.current.get(lastToast.id);
242
+ if (timerEntry?.timer) {
243
+ clearTimeout(timerEntry.timer);
244
+ }
245
+ timersRef.current.delete(lastToast.id);
246
+
247
+ if (lastToast.onHide) {
248
+ lastToast.onHide();
249
+ }
250
+
251
+ if (isWeb) {
252
+ dispatch({ type: 'MARK_CLOSED', payload: { ids: [lastToast.id] } });
253
+ } else {
254
+ remove([lastToast.id]);
255
+ }
256
+ } else if (ids === 'all') {
257
+ timersRef.current.forEach((entry) => {
258
+ if (entry.timer) clearTimeout(entry.timer);
259
+ });
260
+ timersRef.current.clear();
261
+
262
+ const targetIds: string[] = [];
263
+ toasts.forEach((toast) => {
264
+ if (toast.pendingRemoval) return;
265
+ targetIds.push(toast.id);
266
+ if (toast.onHide) {
267
+ toast.onHide();
268
+ }
269
+ });
270
+
271
+ if (targetIds.length === 0) return;
272
+
273
+ if (isWeb) {
274
+ dispatch({ type: 'MARK_CLOSED', payload: { ids: targetIds } });
275
+ } else {
276
+ remove(targetIds);
277
+ }
278
+ } else {
279
+ const idsArray = Array.isArray(ids) ? ids : [ids];
280
+ const targetIds: string[] = [];
281
+
282
+ idsArray.forEach((id) => {
283
+ const toast = toasts.find((t) => t.id === id);
284
+ if (!toast || toast.pendingRemoval) return;
285
+
286
+ const timerEntry = timersRef.current.get(id);
287
+ if (timerEntry?.timer) {
288
+ clearTimeout(timerEntry.timer);
289
+ }
290
+ timersRef.current.delete(id);
291
+
292
+ targetIds.push(id);
293
+ if (toast.onHide) {
294
+ toast.onHide();
295
+ }
296
+ });
297
+
298
+ if (targetIds.length === 0) return;
299
+
300
+ if (isWeb) {
301
+ dispatch({ type: 'MARK_CLOSED', payload: { ids: targetIds } });
302
+ } else {
303
+ remove(targetIds);
304
+ }
305
+ }
306
+ },
307
+ [remove, toasts],
308
+ );
309
+
310
+ // Keep hide ref up to date
311
+ hideRef.current = hide;
312
+
313
+ /**
314
+ * Pause all running auto-dismiss timers (WCAG 2.2.1).
315
+ * Stores elapsed time so resumeAll can restart with the remainder.
316
+ */
317
+ const pauseAll = useCallback(() => {
318
+ if (pausedRef.current) return;
319
+ pausedRef.current = true;
320
+
321
+ const now = Date.now();
322
+ timersRef.current.forEach((entry) => {
323
+ if (entry.timer) {
324
+ clearTimeout(entry.timer);
325
+ entry.timer = null;
326
+ }
327
+ const elapsed = now - entry.startedAt;
328
+ entry.remaining = Math.max(0, entry.remaining - elapsed);
329
+ });
330
+ }, []);
331
+
332
+ /**
333
+ * Resume all paused auto-dismiss timers with their remaining duration.
334
+ */
335
+ const resumeAll = useCallback(() => {
336
+ pausedRef.current = false;
337
+
338
+ const now = Date.now();
339
+ timersRef.current.forEach((entry, id) => {
340
+ if (entry.timer) return;
341
+ if (entry.remaining <= 0) {
342
+ if (hideRef.current) hideRef.current(id);
343
+ timersRef.current.delete(id);
344
+ return;
345
+ }
346
+ entry.startedAt = now;
347
+ entry.timer = setTimeout(() => {
348
+ if (hideRef.current) hideRef.current(id);
349
+ timersRef.current.delete(id);
350
+ }, entry.remaining);
351
+ });
352
+ }, []);
353
+
354
+ const lifecycleContextValue = useMemo<ToastLifecycleContextValue>(() => {
355
+ const pending = new Set<string>();
356
+ let frontmostId: string | null = null;
357
+ for (const toast of toasts) {
358
+ if (toast.pendingRemoval) {
359
+ pending.add(toast.id);
360
+ } else {
361
+ frontmostId = toast.id;
362
+ }
363
+ }
364
+ return {
365
+ remove: (id: string) => remove([id]),
366
+ pendingRemovalIds: pending,
367
+ frontmostId,
368
+ };
369
+ }, [remove, toasts]);
370
+
371
+ /**
372
+ * Show a toast
373
+ * Supports three usage patterns:
374
+ * 1. Simple string: toast.show('This is toast')
375
+ * 2. Config object: toast.show({ label, color, ... })
376
+ * 3. Custom component: toast.show({ component: (props) => <Toast>...</Toast> })
377
+ */
378
+ const show = useCallback(
379
+ (options: string | ToastShowOptions): string => {
380
+ let normalizedOptions: ToastShowOptionsWithComponent;
381
+ let duration: number | 'persistent' | undefined = DEFAULT_DURATION; // Default duration
382
+ let explicitId: string | undefined;
383
+
384
+ // Case 1: Simple string
385
+ if (typeof options === 'string') {
386
+ normalizedOptions = {
387
+ id: undefined,
388
+ component: createStringToastComponent(options, globalConfig),
389
+ duration: DEFAULT_DURATION,
390
+ };
391
+ duration = DEFAULT_DURATION;
392
+ explicitId = undefined;
393
+ }
394
+ // Case 2: Config object without component
395
+ else if (!('component' in options) || options.component === undefined) {
396
+ const config = options as ToastShowConfig;
397
+ duration = config.duration ?? DEFAULT_DURATION;
398
+ explicitId = config.id;
399
+ normalizedOptions = {
400
+ id: config.id,
401
+ component: createConfigToastComponent(config, globalConfig),
402
+ duration,
403
+ onShow: config.onShow,
404
+ onHide: config.onHide,
405
+ };
406
+ }
407
+ // Case 3: Config object with component (existing behavior)
408
+ else {
409
+ normalizedOptions = options;
410
+ duration = normalizedOptions.duration ?? DEFAULT_DURATION;
411
+ explicitId = normalizedOptions.id;
412
+ }
413
+
414
+ const id =
415
+ normalizedOptions.id ?? `toast-${String(Date.now())}-${String(idCounter.current++)}`;
416
+
417
+ // If an explicit ID was provided, check if a toast with that ID already exists
418
+ // If it exists, skip adding a new toast and return the existing ID
419
+ if (explicitId !== undefined) {
420
+ const existingToast = toasts.find((toast) => toast.id === explicitId);
421
+ if (existingToast) {
422
+ return existingToast.id;
423
+ }
424
+ }
425
+
426
+ dispatch({
427
+ type: 'SHOW',
428
+ payload: {
429
+ id,
430
+ component: normalizedOptions.component,
431
+ duration,
432
+ onShow: normalizedOptions.onShow,
433
+ onHide: normalizedOptions.onHide,
434
+ },
435
+ });
436
+
437
+ if (normalizedOptions.onShow) {
438
+ normalizedOptions.onShow();
439
+ }
440
+
441
+ // Set up auto-dismiss timeout synchronously
442
+ if (
443
+ duration !== 'persistent' &&
444
+ typeof duration === 'number' &&
445
+ !Number.isNaN(duration) &&
446
+ duration >= 0 &&
447
+ duration !== Infinity
448
+ ) {
449
+ // Handle immediate dismissal
450
+ if (duration === 0) {
451
+ if (hideRef.current) {
452
+ hideRef.current(id);
453
+ }
454
+ } else {
455
+ const now = Date.now();
456
+ const timer = setTimeout(() => {
457
+ if (hideRef.current) {
458
+ hideRef.current(id);
459
+ }
460
+ timersRef.current.delete(id);
461
+ }, duration);
462
+ const entry = {
463
+ startedAt: now,
464
+ remaining: duration,
465
+ timer: timer as NodeJS.Timeout | null,
466
+ duration,
467
+ };
468
+ timersRef.current.set(id, entry);
469
+
470
+ if (pausedRef.current) {
471
+ clearTimeout(timer);
472
+ entry.timer = null;
473
+ }
474
+ }
475
+ }
476
+
477
+ return id;
478
+ },
479
+ [toasts, globalConfig],
480
+ );
481
+
482
+ const contextValue = useMemo<ToasterContextValue>(
483
+ () => ({
484
+ toast: {
485
+ show,
486
+ hide,
487
+ pauseAll,
488
+ resumeAll,
489
+ },
490
+ isToastVisible,
491
+ }),
492
+ [show, hide, pauseAll, resumeAll, isToastVisible],
493
+ );
494
+
495
+ return (
496
+ <ToastConfigContext.Provider value={globalConfig}>
497
+ <ToasterContext.Provider value={contextValue}>
498
+ <ToastLifecycleContext.Provider value={lifecycleContextValue}>
499
+ {children}
500
+ {toasts.length > 0 && (
501
+ <InsetsContainer
502
+ insets={insets}
503
+ announceLabel={announceLabel}
504
+ contentWrapper={contentWrapper}
505
+ disableFullWindowOverlay={disableFullWindowOverlay}
506
+ unstable_accessibilityContainerViewIsModal={
507
+ unstable_accessibilityContainerViewIsModal
508
+ }
509
+ pauseAll={pauseAll}
510
+ resumeAll={resumeAll}
511
+ hide={hide}
512
+ >
513
+ <View className="flex-1">
514
+ {toasts.map((toastItem, index) => (
515
+ <ToastItemRenderer
516
+ key={toastItem.id}
517
+ toastItem={toastItem}
518
+ show={show}
519
+ hide={hide}
520
+ index={index}
521
+ total={total}
522
+ heights={heights}
523
+ maxVisibleToasts={maxVisibleToasts}
524
+ />
525
+ ))}
526
+ </View>
527
+ </InsetsContainer>
528
+ )}
529
+ </ToastLifecycleContext.Provider>
530
+ </ToasterContext.Provider>
531
+ </ToastConfigContext.Provider>
532
+ );
533
+ }
534
+
535
+ /**
536
+ * Hook to access toast functionality
537
+ *
538
+ * @returns Object containing toast manager and visibility state
539
+ *
540
+ * @example
541
+ * ```tsx
542
+ * const { toast, isToastVisible } = useToast();
543
+ *
544
+ * // Show a toast
545
+ * toast.show({ component: <Toast>Hello</Toast> });
546
+ *
547
+ * // Hide a toast
548
+ * toast.hide('my-toast');
549
+ *
550
+ * // Check if any toast is visible
551
+ * if (isToastVisible) {
552
+ * console.log('A toast is currently displayed');
553
+ * }
554
+ * ```
555
+ */
556
+ export function useToast() {
557
+ const context = useContext(ToasterContext);
558
+
559
+ if (!context) {
560
+ throw new Error('useToast must be used within a ToastProvider provider');
561
+ }
562
+
563
+ return {
564
+ toast: context.toast,
565
+ isToastVisible: context.isToastVisible,
566
+ };
567
+ }
@@ -0,0 +1,40 @@
1
+ import type { ToastAction, ToastItem } from './types';
2
+
3
+ /**
4
+ * Reducer for managing toast state
5
+ *
6
+ * Removal is a two-phase flow so web can play its CSS exit transition
7
+ * before the DOM node is unmounted:
8
+ * `MARK_CLOSED` → flips `pendingRemoval: true` (node stays mounted).
9
+ * `REMOVE` → filters the toast out of the array (node unmounts).
10
+ *
11
+ * Native dispatches `REMOVE` directly since Reanimated's `exiting`
12
+ * layout animation plays before unmount without a lingering state.
13
+ */
14
+ export function toastReducer(state: ToastItem[], action: ToastAction): ToastItem[] {
15
+ switch (action.type) {
16
+ case 'SHOW': {
17
+ const filtered = state.filter((toast) => toast.id !== action.payload.id);
18
+ return [...filtered, action.payload];
19
+ }
20
+
21
+ case 'MARK_CLOSED': {
22
+ const idsSet = new Set(action.payload.ids);
23
+ // Early out preserves the array reference when nothing would change,
24
+ // which keeps `useMemo(..., [toasts])` consumers stable.
25
+ const hasChange = state.some((toast) => idsSet.has(toast.id) && !toast.pendingRemoval);
26
+ if (!hasChange) return state;
27
+ return state.map((toast) =>
28
+ idsSet.has(toast.id) && !toast.pendingRemoval ? { ...toast, pendingRemoval: true } : toast,
29
+ );
30
+ }
31
+
32
+ case 'REMOVE': {
33
+ const idsSet = new Set(action.payload.ids);
34
+ return state.filter((toast) => !idsSet.has(toast.id));
35
+ }
36
+
37
+ default:
38
+ return state;
39
+ }
40
+ }
@@ -0,0 +1,25 @@
1
+ import { createContext, useContext } from 'react';
2
+ import type { ToastGlobalConfig } from './types';
3
+
4
+ /**
5
+ * Context for global toast configuration
6
+ * Extracted to separate file to avoid circular dependencies with toast component
7
+ */
8
+ const ToastConfigContext = createContext<ToastGlobalConfig | undefined>(undefined);
9
+
10
+ /**
11
+ * Hook to access global toast configuration
12
+ *
13
+ * @returns Global toast configuration or undefined if not set
14
+ *
15
+ * @example
16
+ * ```tsx
17
+ * const globalConfig = useToastConfig();
18
+ * // Use globalConfig.color, globalConfig.placement, etc.
19
+ * ```
20
+ */
21
+ export function useToastConfig(): ToastGlobalConfig | undefined {
22
+ return useContext(ToastConfigContext);
23
+ }
24
+
25
+ export { ToastConfigContext };
@@ -0,0 +1,40 @@
1
+ import { memo } from 'react';
2
+ import type { ToastItemRendererProps } from './types';
3
+
4
+ /**
5
+ * Memoized toast item component to prevent unnecessary re-renders
6
+ * Only re-renders when the toast item itself changes
7
+ */
8
+ export const ToastItemRenderer = memo(
9
+ ({ toastItem, show, hide, index, total, heights, maxVisibleToasts }: ToastItemRendererProps) => {
10
+ if (typeof toastItem.component !== 'function') {
11
+ throw new Error('Toast component must be a function that receives ToastComponentProps');
12
+ }
13
+
14
+ const content = toastItem.component({
15
+ id: toastItem.id,
16
+ index,
17
+ total,
18
+ heights,
19
+ maxVisibleToasts,
20
+ show,
21
+ hide,
22
+ });
23
+
24
+ return content;
25
+ },
26
+ (prevProps, nextProps) => {
27
+ // Only re-render if the toast ID, component reference, index, or
28
+ // pending-removal flag changed. `pendingRemoval` flips when the
29
+ // provider marks a toast closed on web so the Toast component can
30
+ // play its exit transition — the renderer must propagate that
31
+ // change even though the id and component reference are unchanged.
32
+ // show, hide, total, and heights are stable references.
33
+ return (
34
+ prevProps.toastItem.id === nextProps.toastItem.id &&
35
+ prevProps.toastItem.component === nextProps.toastItem.component &&
36
+ prevProps.toastItem.pendingRemoval === nextProps.toastItem.pendingRemoval &&
37
+ prevProps.index === nextProps.index
38
+ );
39
+ },
40
+ );