@cdx-ui/components 0.1.3 → 0.1.4

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 (144) hide show
  1. package/coverage.svg +1 -1
  2. package/lib/commonjs/components/Avatar/index.js +30 -8
  3. package/lib/commonjs/components/Avatar/index.js.map +1 -1
  4. package/lib/commonjs/components/Avatar/styles.js +163 -7
  5. package/lib/commonjs/components/Avatar/styles.js.map +1 -1
  6. package/lib/commonjs/components/CLAUDE.md +2 -1
  7. package/lib/commonjs/components/Chip/styles.js +1 -1
  8. package/lib/commonjs/components/Chip/styles.js.map +1 -1
  9. package/lib/commonjs/components/Drawer/animation.js +13 -0
  10. package/lib/commonjs/components/Drawer/animation.js.map +1 -0
  11. package/lib/commonjs/components/Drawer/index.js +739 -0
  12. package/lib/commonjs/components/Drawer/index.js.map +1 -0
  13. package/lib/commonjs/components/Drawer/styles.js +185 -0
  14. package/lib/commonjs/components/Drawer/styles.js.map +1 -0
  15. package/lib/commonjs/components/Drawer/utils.js +95 -0
  16. package/lib/commonjs/components/Drawer/utils.js.map +1 -0
  17. package/lib/commonjs/components/InsuranceDisclosure/styles.js +1 -1
  18. package/lib/commonjs/components/InsuranceDisclosure/styles.js.map +1 -1
  19. package/lib/commonjs/components/Menu/index.js +187 -61
  20. package/lib/commonjs/components/Menu/index.js.map +1 -1
  21. package/lib/commonjs/components/Menu/styles.js +32 -37
  22. package/lib/commonjs/components/Menu/styles.js.map +1 -1
  23. package/lib/commonjs/components/Select/index.js +2 -10
  24. package/lib/commonjs/components/Select/index.js.map +1 -1
  25. package/lib/commonjs/components/Tabs/styles.js +9 -10
  26. package/lib/commonjs/components/Tabs/styles.js.map +1 -1
  27. package/lib/commonjs/components/Toast/styles.js +1 -1
  28. package/lib/commonjs/components/Toast/styles.js.map +1 -1
  29. package/lib/commonjs/components/index.js +12 -0
  30. package/lib/commonjs/components/index.js.map +1 -1
  31. package/lib/commonjs/figma/Avatar.figma.js +18 -4
  32. package/lib/commonjs/figma/Avatar.figma.js.map +1 -1
  33. package/lib/commonjs/figma/Drawer.Content.figma.js +29 -0
  34. package/lib/commonjs/figma/Drawer.Content.figma.js.map +1 -0
  35. package/lib/commonjs/figma/Drawer.Description.figma.js +20 -0
  36. package/lib/commonjs/figma/Drawer.Description.figma.js.map +1 -0
  37. package/lib/commonjs/figma/Drawer.Footer.figma.js +41 -0
  38. package/lib/commonjs/figma/Drawer.Footer.figma.js.map +1 -0
  39. package/lib/commonjs/figma/Drawer.Header.figma.js +45 -0
  40. package/lib/commonjs/figma/Drawer.Header.figma.js.map +1 -0
  41. package/lib/commonjs/figma/Drawer.Title.figma.js +20 -0
  42. package/lib/commonjs/figma/Drawer.Title.figma.js.map +1 -0
  43. package/lib/commonjs/figma/Drawer.figma.js +41 -0
  44. package/lib/commonjs/figma/Drawer.figma.js.map +1 -0
  45. package/lib/commonjs/figma/Menu.Item.figma.js +33 -5
  46. package/lib/commonjs/figma/Menu.Item.figma.js.map +1 -1
  47. package/lib/commonjs/figma/icons.figma.batch.json +262 -2
  48. package/lib/module/components/Avatar/index.js +30 -8
  49. package/lib/module/components/Avatar/index.js.map +1 -1
  50. package/lib/module/components/Avatar/styles.js +163 -7
  51. package/lib/module/components/Avatar/styles.js.map +1 -1
  52. package/lib/module/components/CLAUDE.md +2 -1
  53. package/lib/module/components/Chip/styles.js +1 -1
  54. package/lib/module/components/Chip/styles.js.map +1 -1
  55. package/lib/module/components/Drawer/animation.js +7 -0
  56. package/lib/module/components/Drawer/animation.js.map +1 -0
  57. package/lib/module/components/Drawer/index.js +724 -0
  58. package/lib/module/components/Drawer/index.js.map +1 -0
  59. package/lib/module/components/Drawer/styles.js +181 -0
  60. package/lib/module/components/Drawer/styles.js.map +1 -0
  61. package/lib/module/components/Drawer/utils.js +86 -0
  62. package/lib/module/components/Drawer/utils.js.map +1 -0
  63. package/lib/module/components/InsuranceDisclosure/styles.js +1 -1
  64. package/lib/module/components/InsuranceDisclosure/styles.js.map +1 -1
  65. package/lib/module/components/Menu/index.js +178 -52
  66. package/lib/module/components/Menu/index.js.map +1 -1
  67. package/lib/module/components/Menu/styles.js +31 -36
  68. package/lib/module/components/Menu/styles.js.map +1 -1
  69. package/lib/module/components/Select/index.js +3 -11
  70. package/lib/module/components/Select/index.js.map +1 -1
  71. package/lib/module/components/Tabs/styles.js +9 -10
  72. package/lib/module/components/Tabs/styles.js.map +1 -1
  73. package/lib/module/components/Toast/styles.js +1 -1
  74. package/lib/module/components/Toast/styles.js.map +1 -1
  75. package/lib/module/components/index.js +1 -0
  76. package/lib/module/components/index.js.map +1 -1
  77. package/lib/module/figma/Avatar.figma.js +18 -4
  78. package/lib/module/figma/Avatar.figma.js.map +1 -1
  79. package/lib/module/figma/Drawer.Content.figma.js +23 -0
  80. package/lib/module/figma/Drawer.Content.figma.js.map +1 -0
  81. package/lib/module/figma/Drawer.Description.figma.js +14 -0
  82. package/lib/module/figma/Drawer.Description.figma.js.map +1 -0
  83. package/lib/module/figma/Drawer.Footer.figma.js +35 -0
  84. package/lib/module/figma/Drawer.Footer.figma.js.map +1 -0
  85. package/lib/module/figma/Drawer.Header.figma.js +39 -0
  86. package/lib/module/figma/Drawer.Header.figma.js.map +1 -0
  87. package/lib/module/figma/Drawer.Title.figma.js +14 -0
  88. package/lib/module/figma/Drawer.Title.figma.js.map +1 -0
  89. package/lib/module/figma/Drawer.figma.js +35 -0
  90. package/lib/module/figma/Drawer.figma.js.map +1 -0
  91. package/lib/module/figma/Menu.Item.figma.js +33 -5
  92. package/lib/module/figma/Menu.Item.figma.js.map +1 -1
  93. package/lib/module/figma/icons.figma.batch.json +262 -2
  94. package/lib/typescript/components/Avatar/index.d.ts +19 -1
  95. package/lib/typescript/components/Avatar/index.d.ts.map +1 -1
  96. package/lib/typescript/components/Avatar/styles.d.ts +10 -1
  97. package/lib/typescript/components/Avatar/styles.d.ts.map +1 -1
  98. package/lib/typescript/components/Drawer/animation.d.ts +9 -0
  99. package/lib/typescript/components/Drawer/animation.d.ts.map +1 -0
  100. package/lib/typescript/components/Drawer/index.d.ts +106 -0
  101. package/lib/typescript/components/Drawer/index.d.ts.map +1 -0
  102. package/lib/typescript/components/Drawer/styles.d.ts +32 -0
  103. package/lib/typescript/components/Drawer/styles.d.ts.map +1 -0
  104. package/lib/typescript/components/Drawer/utils.d.ts +23 -0
  105. package/lib/typescript/components/Drawer/utils.d.ts.map +1 -0
  106. package/lib/typescript/components/InsuranceDisclosure/styles.d.ts.map +1 -1
  107. package/lib/typescript/components/Menu/index.d.ts +29 -2
  108. package/lib/typescript/components/Menu/index.d.ts.map +1 -1
  109. package/lib/typescript/components/Menu/styles.d.ts +6 -2
  110. package/lib/typescript/components/Menu/styles.d.ts.map +1 -1
  111. package/lib/typescript/components/Select/index.d.ts.map +1 -1
  112. package/lib/typescript/components/Tabs/styles.d.ts.map +1 -1
  113. package/lib/typescript/components/index.d.ts +1 -0
  114. package/lib/typescript/components/index.d.ts.map +1 -1
  115. package/lib/typescript/figma/Avatar.figma.d.ts.map +1 -1
  116. package/lib/typescript/figma/Drawer.Content.figma.d.ts +8 -0
  117. package/lib/typescript/figma/Drawer.Content.figma.d.ts.map +1 -0
  118. package/lib/typescript/figma/Drawer.Description.figma.d.ts +8 -0
  119. package/lib/typescript/figma/Drawer.Description.figma.d.ts.map +1 -0
  120. package/lib/typescript/figma/Drawer.Footer.figma.d.ts +11 -0
  121. package/lib/typescript/figma/Drawer.Footer.figma.d.ts.map +1 -0
  122. package/lib/typescript/figma/Drawer.Header.figma.d.ts +8 -0
  123. package/lib/typescript/figma/Drawer.Header.figma.d.ts.map +1 -0
  124. package/lib/typescript/figma/Drawer.Title.figma.d.ts +8 -0
  125. package/lib/typescript/figma/Drawer.Title.figma.d.ts.map +1 -0
  126. package/lib/typescript/figma/Drawer.figma.d.ts +8 -0
  127. package/lib/typescript/figma/Drawer.figma.d.ts.map +1 -0
  128. package/lib/typescript/figma/Menu.Item.figma.d.ts.map +1 -1
  129. package/package.json +5 -5
  130. package/src/components/Avatar/index.tsx +45 -9
  131. package/src/components/Avatar/styles.ts +106 -23
  132. package/src/components/CLAUDE.md +2 -1
  133. package/src/components/Chip/styles.ts +1 -1
  134. package/src/components/Drawer/animation.ts +11 -0
  135. package/src/components/Drawer/index.tsx +896 -0
  136. package/src/components/Drawer/styles.ts +188 -0
  137. package/src/components/Drawer/utils.ts +83 -0
  138. package/src/components/InsuranceDisclosure/styles.ts +1 -3
  139. package/src/components/Menu/index.tsx +204 -62
  140. package/src/components/Menu/styles.ts +44 -54
  141. package/src/components/Select/index.tsx +7 -9
  142. package/src/components/Tabs/styles.ts +9 -10
  143. package/src/components/Toast/styles.ts +1 -1
  144. package/src/components/index.ts +1 -0
@@ -0,0 +1,896 @@
1
+ import {
2
+ Children,
3
+ forwardRef,
4
+ isValidElement,
5
+ useCallback,
6
+ useEffect,
7
+ useMemo,
8
+ useRef,
9
+ useState,
10
+ type ComponentRef,
11
+ type ReactNode,
12
+ } from 'react';
13
+ import {
14
+ BackHandler,
15
+ Platform,
16
+ Pressable,
17
+ ScrollView,
18
+ StyleSheet,
19
+ useWindowDimensions,
20
+ View,
21
+ type ScrollViewProps,
22
+ type TextProps,
23
+ type ViewProps,
24
+ } from 'react-native';
25
+ import Animated, { useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated';
26
+ import { FocusScope } from '@react-native-aria/focus';
27
+ import { Close as CloseIcon } from '@cdx-ui/icons';
28
+ import {
29
+ DrawerClose as DrawerClosePrimitive,
30
+ DrawerOverlay as DrawerOverlayPrimitive,
31
+ DrawerPortal as DrawerPortalPrimitive,
32
+ DrawerRoot as DrawerRootPrimitive,
33
+ FullWindowOverlay,
34
+ MODAL_OVERLAY_Z_INDEX,
35
+ Slot,
36
+ dataAttributes,
37
+ mergeDataAttributes,
38
+ useDrawer,
39
+ useEscapeKey,
40
+ useOverlayStackRegistration,
41
+ type IDrawerCloseProps,
42
+ type IDrawerRootProps,
43
+ } from '@cdx-ui/primitives';
44
+ import { cn, mergeRefs, ParentContext, useParentContext, useStyleContext } from '@cdx-ui/utils';
45
+ import {
46
+ usePopupOverlayAnimation,
47
+ usePopupRootAnimation,
48
+ type AnimationRootDisableAll,
49
+ } from '../../animation';
50
+ import {
51
+ AnimationSettingsProvider,
52
+ useAnimationSettings,
53
+ } from '../../providers/animation-settings';
54
+ import { Heading } from '../Heading';
55
+ import { Icon } from '../Icon';
56
+ import { Text } from '../Text';
57
+ import { DrawerAnimationProvider, useDrawerAnimation } from './animation';
58
+ import {
59
+ type DrawerFooterVariantProps,
60
+ type DrawerHeaderVariantProps,
61
+ type DrawerPlacement,
62
+ type DrawerVariantProps,
63
+ drawerCloseIconVariants,
64
+ drawerCloseVariants,
65
+ drawerContentInnerVariants,
66
+ drawerContentVariants,
67
+ drawerDescriptionVariants,
68
+ drawerFooterVariants,
69
+ drawerHeaderContentVariants,
70
+ drawerHeaderVariants,
71
+ drawerLeadingSlotVariants,
72
+ drawerOverlayVariants,
73
+ drawerPanelVariants,
74
+ drawerPortalVariants,
75
+ drawerTitleVariants,
76
+ } from './styles';
77
+ import {
78
+ DRAWER_EDGE_PADDING,
79
+ DRAWER_ENTER_EASING,
80
+ DRAWER_ENTER_MS,
81
+ DRAWER_EXIT_EASING,
82
+ DRAWER_EXIT_MS,
83
+ DRAWER_WIDE_BREAKPOINT,
84
+ WEB_TRANSITION_SAFETY_MS,
85
+ getDrawerSlideKeyframes,
86
+ isHorizontalDrawerPlacement,
87
+ resolveDrawerPlacement,
88
+ useClampedDrawerExtent,
89
+ useSafeInsets,
90
+ } from './utils';
91
+
92
+ export type { DrawerPlacement } from './styles';
93
+
94
+ export { useDrawer } from '@cdx-ui/primitives';
95
+
96
+ const IS_WEB = Platform.OS === 'web';
97
+
98
+ type DrawerDataState = 'entering' | 'open' | 'closed';
99
+
100
+ // =============================================================================
101
+ // STYLE CONTEXT
102
+ // =============================================================================
103
+
104
+ const SCOPE = 'DRAWER';
105
+
106
+ const useDrawerStyleContext = () => useStyleContext(SCOPE) as DrawerVariantProps;
107
+
108
+ // =============================================================================
109
+ // ANIMATED PRIMITIVES
110
+ // =============================================================================
111
+
112
+ const AnimatedOverlay = Animated.createAnimatedComponent(DrawerOverlayPrimitive);
113
+
114
+ // =============================================================================
115
+ // STYLED ROOT
116
+ // =============================================================================
117
+
118
+ export interface DrawerRootProps
119
+ extends
120
+ Omit<ViewProps, 'children' | 'role'>,
121
+ IDrawerRootProps,
122
+ Omit<DrawerVariantProps, 'placement'> {
123
+ className?: string;
124
+ animation?: AnimationRootDisableAll;
125
+ /**
126
+ * Edge the panel attaches to. Applies to both `modal` and `permanent`.
127
+ * @default 'right'
128
+ */
129
+ placement?: DrawerPlacement;
130
+ /**
131
+ * Stacking order for the modal overlay layer (backdrop + panel).
132
+ * Defaults to {@link MODAL_OVERLAY_Z_INDEX} (10000).
133
+ */
134
+ zIndex?: number;
135
+ }
136
+
137
+ const DrawerRoot = forwardRef<View, DrawerRootProps>(
138
+ (
139
+ {
140
+ variant = 'permanent',
141
+ size = 'default',
142
+ placement: placementProp,
143
+ fullBleed = false,
144
+ animation,
145
+ className,
146
+ children,
147
+ style,
148
+ isOpen,
149
+ defaultOpen,
150
+ onOpenChange,
151
+ closeOnBackdropPress = true,
152
+ closeOnEscKey = true,
153
+ zIndex = MODAL_OVERLAY_Z_INDEX,
154
+ ...props
155
+ },
156
+ ref,
157
+ ) => {
158
+ const placement = resolveDrawerPlacement(placementProp);
159
+ const parent = useParentContext();
160
+ const ctx = useMemo(
161
+ () => ({ ...parent, [SCOPE]: { variant, size, placement, fullBleed } }),
162
+ [parent, variant, size, placement, fullBleed],
163
+ );
164
+
165
+ const { progress, isAllAnimationsDisabled } = usePopupRootAnimation({ animation });
166
+ const panelExtent = useClampedDrawerExtent(size, placement);
167
+
168
+ const animationContextValue = useMemo(() => ({ progress }), [progress]);
169
+
170
+ const animationSettingsContextValue = useMemo(
171
+ () => ({ isAllAnimationsDisabled }),
172
+ [isAllAnimationsDisabled],
173
+ );
174
+
175
+ if (variant === 'permanent') {
176
+ return (
177
+ <AnimationSettingsProvider value={animationSettingsContextValue}>
178
+ <ParentContext.Provider value={ctx}>
179
+ <DrawerRootPrimitive
180
+ ref={ref}
181
+ isOpen={isOpen}
182
+ defaultOpen={defaultOpen ?? true}
183
+ onOpenChange={onOpenChange}
184
+ closeOnBackdropPress={false}
185
+ closeOnEscKey={false}
186
+ className={cn('self-stretch overflow-hidden', className)}
187
+ style={style}
188
+ {...props}
189
+ {...mergeDataAttributes(props, dataAttributes({ slot: 'drawer', placement }))}
190
+ >
191
+ <DrawerPermanentPanel size={size} placement={placement} panelExtent={panelExtent}>
192
+ {children}
193
+ </DrawerPermanentPanel>
194
+ </DrawerRootPrimitive>
195
+ </ParentContext.Provider>
196
+ </AnimationSettingsProvider>
197
+ );
198
+ }
199
+
200
+ return (
201
+ <AnimationSettingsProvider value={animationSettingsContextValue}>
202
+ <DrawerAnimationProvider value={animationContextValue}>
203
+ <ParentContext.Provider value={ctx}>
204
+ <DrawerRootPrimitive
205
+ ref={ref}
206
+ isOpen={isOpen}
207
+ defaultOpen={defaultOpen}
208
+ onOpenChange={onOpenChange}
209
+ closeOnBackdropPress={closeOnBackdropPress}
210
+ closeOnEscKey={closeOnEscKey}
211
+ className={className}
212
+ style={style}
213
+ {...props}
214
+ {...mergeDataAttributes(props, dataAttributes({ slot: 'drawer', placement }))}
215
+ >
216
+ <DrawerPopup zIndex={zIndex}>{children}</DrawerPopup>
217
+ </DrawerRootPrimitive>
218
+ </ParentContext.Provider>
219
+ </DrawerAnimationProvider>
220
+ </AnimationSettingsProvider>
221
+ );
222
+ },
223
+ );
224
+
225
+ DrawerRoot.displayName = 'Drawer';
226
+
227
+ interface DrawerPermanentPanelProps {
228
+ size: DrawerVariantProps['size'];
229
+ placement: DrawerPlacement;
230
+ panelExtent: number;
231
+ children?: ReactNode;
232
+ }
233
+
234
+ const DrawerPermanentPanel = ({
235
+ size,
236
+ placement,
237
+ panelExtent,
238
+ children,
239
+ }: DrawerPermanentPanelProps) => {
240
+ const { open } = useDrawer();
241
+ const { isAllAnimationsDisabled } = useAnimationSettings();
242
+ const isHorizontal = isHorizontalDrawerPlacement(placement);
243
+ const extent = useSharedValue(open ? panelExtent : 0);
244
+ const hasMounted = useRef(false);
245
+
246
+ useEffect(() => {
247
+ const target = open ? panelExtent : 0;
248
+
249
+ if (!hasMounted.current) {
250
+ hasMounted.current = true;
251
+ extent.set(target);
252
+ return;
253
+ }
254
+
255
+ if (isAllAnimationsDisabled) {
256
+ extent.set(target);
257
+ return;
258
+ }
259
+
260
+ extent.set(
261
+ withTiming(target, {
262
+ duration: open ? DRAWER_ENTER_MS : DRAWER_EXIT_MS,
263
+ easing: open ? DRAWER_ENTER_EASING : DRAWER_EXIT_EASING,
264
+ }),
265
+ );
266
+ }, [extent, isAllAnimationsDisabled, open, panelExtent]);
267
+
268
+ const animatedStyle = useAnimatedStyle(() => {
269
+ const value = extent.get();
270
+ return isHorizontal ? { width: value } : { height: value };
271
+ }, [isHorizontal]);
272
+
273
+ return (
274
+ <Animated.View
275
+ className={drawerPanelVariants({ size, variant: 'permanent', placement })}
276
+ pointerEvents={open ? 'auto' : 'none'}
277
+ style={animatedStyle}
278
+ >
279
+ <View
280
+ className="h-full"
281
+ style={isHorizontal ? { width: panelExtent } : { height: panelExtent }}
282
+ >
283
+ {open ? children : null}
284
+ </View>
285
+ </Animated.View>
286
+ );
287
+ };
288
+
289
+ // =============================================================================
290
+ // INTERNAL POPUP (portal + overlay + panel — not a Figma slot)
291
+ // =============================================================================
292
+
293
+ interface DrawerPopupProps {
294
+ className?: string;
295
+ children?: ReactNode;
296
+ disableFullWindowOverlay?: boolean;
297
+ zIndex?: number;
298
+ }
299
+
300
+ const DrawerPopup = forwardRef<View, DrawerPopupProps>(
301
+ (
302
+ { className, children, disableFullWindowOverlay = false, zIndex = MODAL_OVERLAY_Z_INDEX },
303
+ ref,
304
+ ) => {
305
+ const { size, placement: placementProp } = useDrawerStyleContext();
306
+ const placement = resolveDrawerPlacement(placementProp);
307
+ const { open, onOpenChange, closeOnEscKey, nativeID } = useDrawer();
308
+ const { width: windowWidth, height: windowHeight } = useWindowDimensions();
309
+ const panelExtent = useClampedDrawerExtent(size, placement);
310
+ const parentContext = useParentContext();
311
+
312
+ const animationContext = useDrawerAnimation();
313
+ const parentAnimationSettings = useAnimationSettings();
314
+ const { isAllAnimationsDisabled } = parentAnimationSettings;
315
+ const panelMotion = useMemo(
316
+ () => getDrawerSlideKeyframes(placement, panelExtent),
317
+ [panelExtent, placement],
318
+ );
319
+
320
+ const fallbackIsDragging = useSharedValue(false);
321
+ const fallbackIsGestureReleaseAnimationRunning = useSharedValue(false);
322
+
323
+ const progress = animationContext.progress;
324
+
325
+ useEffect(() => {
326
+ if (open) {
327
+ progress.set(1);
328
+ }
329
+ }, [open, progress]);
330
+
331
+ const {
332
+ rContainerStyle,
333
+ entering: overlayEntering,
334
+ exiting: overlayExiting,
335
+ } = usePopupOverlayAnimation({
336
+ progress,
337
+ isDragging: fallbackIsDragging,
338
+ isGestureReleaseAnimationRunning: fallbackIsGestureReleaseAnimationRunning,
339
+ });
340
+
341
+ const panelRef = useRef<View | null>(null);
342
+ const prevOpenRef = useRef(open);
343
+ const [isExiting, setIsExiting] = useState(false);
344
+ const [webState, setWebState] = useState<DrawerDataState>(open ? 'entering' : 'closed');
345
+
346
+ const handleClose = useCallback(() => {
347
+ onOpenChange(false);
348
+ }, [onOpenChange]);
349
+
350
+ useEscapeKey(open && closeOnEscKey, handleClose, {
351
+ id: nativeID,
352
+ groupId: nativeID,
353
+ layer: 'modal',
354
+ });
355
+ const { isTopmost } = useOverlayStackRegistration({
356
+ active: open && Platform.OS !== 'web',
357
+ capabilities: ['back'],
358
+ id: nativeID,
359
+ groupId: nativeID,
360
+ layer: 'modal',
361
+ });
362
+
363
+ useEffect(() => {
364
+ if (!open || Platform.OS === 'web') return;
365
+
366
+ const backHandler = BackHandler.addEventListener('hardwareBackPress', () => {
367
+ if (!isTopmost('back')) return false;
368
+ handleClose();
369
+ return true;
370
+ });
371
+
372
+ return () => {
373
+ backHandler.remove();
374
+ };
375
+ }, [handleClose, isTopmost, open]);
376
+
377
+ if (IS_WEB && prevOpenRef.current && !open && !isExiting) {
378
+ setIsExiting(true);
379
+ setWebState('closed');
380
+ }
381
+ if (IS_WEB && !prevOpenRef.current && open) {
382
+ setIsExiting(false);
383
+ setWebState('entering');
384
+ }
385
+ prevOpenRef.current = open;
386
+
387
+ useEffect(() => {
388
+ if (!IS_WEB || !open) return;
389
+ const raf = requestAnimationFrame(() => {
390
+ setWebState('open');
391
+ });
392
+ return () => cancelAnimationFrame(raf);
393
+ }, [open]);
394
+
395
+ useEffect(() => {
396
+ if (!IS_WEB || !isExiting) return;
397
+
398
+ const node = panelRef.current as unknown as HTMLElement | null;
399
+ let done = false;
400
+
401
+ const finish = () => {
402
+ if (done) return;
403
+ done = true;
404
+ setIsExiting(false);
405
+ };
406
+
407
+ const safetyTimer = setTimeout(finish, WEB_TRANSITION_SAFETY_MS);
408
+
409
+ if (!node || typeof node.addEventListener !== 'function') {
410
+ return () => {
411
+ clearTimeout(safetyTimer);
412
+ };
413
+ }
414
+
415
+ const handler = (event: Event) => {
416
+ if (event.target !== node) return;
417
+ const propertyName = (event as unknown as { propertyName?: string }).propertyName ?? '';
418
+ if (propertyName === 'opacity' || propertyName === 'transform') {
419
+ finish();
420
+ }
421
+ };
422
+ node.addEventListener('transitionend', handler);
423
+
424
+ return () => {
425
+ clearTimeout(safetyTimer);
426
+ node.removeEventListener('transitionend', handler);
427
+ };
428
+ }, [isExiting]);
429
+
430
+ const dataState: DrawerDataState = webState;
431
+ const shouldMount = IS_WEB ? open || isExiting : open;
432
+
433
+ if (!shouldMount) {
434
+ return null;
435
+ }
436
+
437
+ const portalClassName = cn(drawerPortalVariants());
438
+ const overlayClassName = cn(drawerOverlayVariants());
439
+ const panelClassName = cn(
440
+ drawerPanelVariants({ size, variant: 'modal', placement }),
441
+ className,
442
+ );
443
+ const panelSizeStyle = isHorizontalDrawerPlacement(placement)
444
+ ? { width: panelExtent, height: windowHeight }
445
+ : { width: windowWidth, height: panelExtent };
446
+ const nativePanelEntering =
447
+ IS_WEB || isAllAnimationsDisabled ? undefined : panelMotion.entering;
448
+ const nativePanelExiting = IS_WEB || isAllAnimationsDisabled ? undefined : panelMotion.exiting;
449
+
450
+ return (
451
+ <DrawerPortalPrimitive forceMount>
452
+ <ParentContext.Provider value={parentContext}>
453
+ <AnimationSettingsProvider value={parentAnimationSettings}>
454
+ <DrawerAnimationProvider value={animationContext}>
455
+ <FullWindowOverlay
456
+ disableFullWindowOverlay={disableFullWindowOverlay}
457
+ unstable_accessibilityContainerViewIsModal
458
+ >
459
+ <View
460
+ className={portalClassName}
461
+ style={{ zIndex }}
462
+ pointerEvents="box-none"
463
+ {...(IS_WEB ? dataAttributes({ state: dataState }) : undefined)}
464
+ >
465
+ <Animated.View
466
+ entering={overlayEntering}
467
+ exiting={overlayExiting}
468
+ style={StyleSheet.absoluteFill}
469
+ >
470
+ <AnimatedOverlay
471
+ forceMount
472
+ testID="drawer-overlay"
473
+ className={overlayClassName}
474
+ style={IS_WEB ? undefined : rContainerStyle}
475
+ {...mergeDataAttributes(
476
+ {},
477
+ dataAttributes({ slot: 'drawer-overlay' }),
478
+ IS_WEB ? dataAttributes({ state: dataState }) : undefined,
479
+ )}
480
+ />
481
+ </Animated.View>
482
+
483
+ <Animated.View
484
+ entering={nativePanelEntering}
485
+ exiting={nativePanelExiting}
486
+ pointerEvents="box-none"
487
+ style={[internalStyles[placement], panelSizeStyle]}
488
+ >
489
+ <FocusScope contain={open} restoreFocus autoFocus={open}>
490
+ <View
491
+ ref={mergeRefs(ref, panelRef)}
492
+ nativeID={nativeID}
493
+ role="dialog"
494
+ aria-modal={true}
495
+ aria-labelledby={`${nativeID}-title`}
496
+ aria-describedby={`${nativeID}-description`}
497
+ className={panelClassName}
498
+ style={panelSizeStyle}
499
+ {...mergeDataAttributes(
500
+ {},
501
+ dataAttributes({ slot: 'drawer', placement }),
502
+ IS_WEB ? dataAttributes({ state: dataState }) : undefined,
503
+ )}
504
+ >
505
+ {children}
506
+ </View>
507
+ </FocusScope>
508
+ </Animated.View>
509
+ </View>
510
+ </FullWindowOverlay>
511
+ </DrawerAnimationProvider>
512
+ </AnimationSettingsProvider>
513
+ </ParentContext.Provider>
514
+ </DrawerPortalPrimitive>
515
+ );
516
+ },
517
+ );
518
+
519
+ DrawerPopup.displayName = 'DrawerPopup';
520
+
521
+ export interface DrawerCloseProps extends IDrawerCloseProps {
522
+ className?: string;
523
+ /** Replaces the default close glyph. */
524
+ children?: ReactNode;
525
+ }
526
+
527
+ const DrawerClose = forwardRef<ComponentRef<typeof Pressable>, DrawerCloseProps>(
528
+ ({ className, accessibilityLabel = 'Close', children, style, ...props }, ref) => (
529
+ <DrawerClosePrimitive
530
+ ref={ref}
531
+ className={cn(drawerCloseVariants(), className)}
532
+ style={style}
533
+ hitSlop={12}
534
+ accessibilityRole="button"
535
+ accessibilityLabel={accessibilityLabel}
536
+ {...props}
537
+ {...mergeDataAttributes(props, dataAttributes({ slot: 'drawer-close' }))}
538
+ >
539
+ {children ?? <Icon as={CloseIcon} className={drawerCloseIconVariants()} />}
540
+ </DrawerClosePrimitive>
541
+ ),
542
+ );
543
+
544
+ DrawerClose.displayName = 'Drawer.Close';
545
+
546
+ // =============================================================================
547
+ // STYLED LEADING SLOT
548
+ // =============================================================================
549
+
550
+ export interface DrawerLeadingSlotProps extends ViewProps {
551
+ asChild?: boolean;
552
+ className?: string;
553
+ 'aria-hidden'?: boolean;
554
+ }
555
+
556
+ const DrawerLeadingSlot = forwardRef<View, DrawerLeadingSlotProps>(
557
+ ({ asChild, className, style, 'aria-hidden': ariaHidden, ...props }, ref) => {
558
+ const Comp = asChild ? Slot : View;
559
+ return (
560
+ <Comp
561
+ ref={ref}
562
+ accessibilityElementsHidden={ariaHidden !== false}
563
+ aria-hidden={ariaHidden ?? true}
564
+ className={cn(drawerLeadingSlotVariants(), className)}
565
+ style={style}
566
+ {...props}
567
+ {...mergeDataAttributes(props, dataAttributes({ slot: 'drawer-leading' }))}
568
+ />
569
+ );
570
+ },
571
+ );
572
+
573
+ DrawerLeadingSlot.displayName = 'Drawer.LeadingSlot';
574
+
575
+ // =============================================================================
576
+ // STYLED HEADER
577
+ // =============================================================================
578
+
579
+ /**
580
+ * Props for `Drawer.Header`.
581
+ *
582
+ * The header lays out its direct children as a fixed row regardless of JSX
583
+ * order: the first `Drawer.LeadingSlot` at the start, every other child
584
+ * (`Drawer.Title`, `Drawer.Description`, custom content) in a text column, and
585
+ * the close control at the end. Additional `Drawer.LeadingSlot` or
586
+ * `Drawer.Close` children are ignored.
587
+ *
588
+ * `Drawer.LeadingSlot` and `Drawer.Close` must be direct children. They are
589
+ * matched by component identity, so wrapping them in `memo()`, an HOC, or a
590
+ * custom component hides them from the header and they render in the text
591
+ * column instead.
592
+ */
593
+ export interface DrawerHeaderProps extends ViewProps, DrawerHeaderVariantProps {
594
+ /**
595
+ * Substitute the header element with the single child. The child is rendered
596
+ * unchanged: no row partition and no close control are applied, and `close`
597
+ * is ignored.
598
+ */
599
+ asChild?: boolean;
600
+ /**
601
+ * Close control rendered at the end of the row.
602
+ *
603
+ * - omitted: renders a composed `Drawer.Close` child if present, otherwise a
604
+ * default `<Drawer.Close />`
605
+ * - `null`: renders no close control
606
+ * - a node: rendered in place of the default and any `Drawer.Close` child
607
+ */
608
+ close?: ReactNode;
609
+ className?: string;
610
+ children?: ReactNode;
611
+ }
612
+
613
+ const renderDrawerHeaderRow = (children: ReactNode, close: ReactNode) => {
614
+ const childArray = Children.toArray(children);
615
+ const leadingChild = childArray.find(
616
+ (child) => isValidElement(child) && child.type === DrawerLeadingSlot,
617
+ );
618
+ const closeChild = childArray.find(
619
+ (child) => isValidElement(child) && child.type === DrawerClose,
620
+ );
621
+ const textChildren = childArray.filter(
622
+ (child) =>
623
+ !isValidElement(child) || (child.type !== DrawerLeadingSlot && child.type !== DrawerClose),
624
+ );
625
+ const closeElement = close === undefined ? (closeChild ?? <DrawerClose />) : close;
626
+
627
+ return (
628
+ <>
629
+ {leadingChild}
630
+ {textChildren.length > 0 ? (
631
+ <View className={drawerHeaderContentVariants()}>{textChildren}</View>
632
+ ) : null}
633
+ {closeElement}
634
+ </>
635
+ );
636
+ };
637
+
638
+ const DrawerHeader = forwardRef<View, DrawerHeaderProps>(
639
+ ({ asChild, close, showSeparator = true, className, children, style, ...props }, ref) => {
640
+ const insets = useSafeInsets();
641
+ const { placement: placementProp, variant } = useDrawerStyleContext();
642
+ const placement = resolveDrawerPlacement(placementProp);
643
+ const paddingTop =
644
+ variant === 'modal' && placement !== 'bottom'
645
+ ? Math.max(insets.top, DRAWER_EDGE_PADDING)
646
+ : DRAWER_EDGE_PADDING;
647
+ const computedClassName = cn(drawerHeaderVariants({ showSeparator }), className);
648
+ const Comp = asChild ? Slot : View;
649
+
650
+ return (
651
+ <Comp
652
+ ref={ref}
653
+ className={computedClassName}
654
+ style={[{ paddingTop }, style]}
655
+ {...props}
656
+ {...mergeDataAttributes(props, dataAttributes({ slot: 'drawer-header' }))}
657
+ >
658
+ {asChild ? children : renderDrawerHeaderRow(children, close)}
659
+ </Comp>
660
+ );
661
+ },
662
+ );
663
+
664
+ DrawerHeader.displayName = 'Drawer.Header';
665
+
666
+ // =============================================================================
667
+ // STYLED TITLE
668
+ // =============================================================================
669
+
670
+ export interface DrawerTitleProps extends TextProps {
671
+ asChild?: boolean;
672
+ className?: string;
673
+ children?: ReactNode;
674
+ }
675
+
676
+ const DrawerTitle = forwardRef<View, DrawerTitleProps>(
677
+ ({ asChild, className, children, style, ...props }, ref) => {
678
+ const { nativeID } = useDrawer();
679
+ const computedClassName = cn(drawerTitleVariants(), className);
680
+
681
+ if (asChild) {
682
+ return (
683
+ <Slot
684
+ ref={ref}
685
+ nativeID={`${nativeID}-title`}
686
+ className={computedClassName}
687
+ style={style}
688
+ {...props}
689
+ {...mergeDataAttributes(props, dataAttributes({ slot: 'drawer-title' }))}
690
+ >
691
+ {children}
692
+ </Slot>
693
+ );
694
+ }
695
+
696
+ return (
697
+ <Heading
698
+ ref={ref}
699
+ size="sm"
700
+ nativeID={`${nativeID}-title`}
701
+ className={computedClassName}
702
+ style={style}
703
+ {...props}
704
+ {...mergeDataAttributes(props, dataAttributes({ slot: 'drawer-title' }))}
705
+ >
706
+ {children}
707
+ </Heading>
708
+ );
709
+ },
710
+ );
711
+
712
+ DrawerTitle.displayName = 'Drawer.Title';
713
+
714
+ // =============================================================================
715
+ // STYLED DESCRIPTION
716
+ // =============================================================================
717
+
718
+ export interface DrawerDescriptionProps extends TextProps {
719
+ asChild?: boolean;
720
+ className?: string;
721
+ children?: ReactNode;
722
+ }
723
+
724
+ const DrawerDescription = forwardRef<View, DrawerDescriptionProps>(
725
+ ({ asChild, className, children, style, ...props }, ref) => {
726
+ const { nativeID } = useDrawer();
727
+ const computedClassName = cn(drawerDescriptionVariants(), className);
728
+ const Comp = asChild ? Slot : Text;
729
+
730
+ return (
731
+ <Comp
732
+ ref={ref}
733
+ nativeID={`${nativeID}-description`}
734
+ size="sm"
735
+ className={computedClassName}
736
+ style={style}
737
+ {...props}
738
+ {...mergeDataAttributes(props, dataAttributes({ slot: 'drawer-description' }))}
739
+ >
740
+ {children}
741
+ </Comp>
742
+ );
743
+ },
744
+ );
745
+
746
+ DrawerDescription.displayName = 'Drawer.Description';
747
+
748
+ // =============================================================================
749
+ // STYLED CONTENT (Figma Drawer.Content — scroll region)
750
+ // =============================================================================
751
+
752
+ export interface DrawerContentProps extends ScrollViewProps {
753
+ className?: string;
754
+ contentClassName?: string;
755
+ children?: ReactNode;
756
+ fullBleed?: boolean;
757
+ }
758
+
759
+ const DrawerContent = forwardRef<ScrollView, DrawerContentProps>(
760
+ (
761
+ {
762
+ className,
763
+ contentClassName,
764
+ children,
765
+ style,
766
+ contentContainerStyle,
767
+ fullBleed: fullBleedProp,
768
+ ...props
769
+ },
770
+ ref,
771
+ ) => {
772
+ const { fullBleed: contextFullBleed } = useDrawerStyleContext();
773
+ const fullBleed = fullBleedProp ?? contextFullBleed ?? false;
774
+
775
+ return (
776
+ <ScrollView
777
+ ref={ref}
778
+ className={cn(drawerContentVariants({ fullBleed }), className)}
779
+ style={style}
780
+ contentContainerClassName={cn(drawerContentInnerVariants(), contentClassName)}
781
+ contentContainerStyle={contentContainerStyle}
782
+ {...props}
783
+ {...mergeDataAttributes(props, dataAttributes({ slot: 'drawer-content' }))}
784
+ >
785
+ {children}
786
+ </ScrollView>
787
+ );
788
+ },
789
+ );
790
+
791
+ DrawerContent.displayName = 'Drawer.Content';
792
+
793
+ // =============================================================================
794
+ // STYLED FOOTER
795
+ // =============================================================================
796
+
797
+ export interface DrawerFooterProps extends ViewProps, DrawerFooterVariantProps {
798
+ asChild?: boolean;
799
+ className?: string;
800
+ children?: ReactNode;
801
+ direction?: 'row' | 'column';
802
+ }
803
+
804
+ const DrawerFooter = forwardRef<View, DrawerFooterProps>(
805
+ (
806
+ {
807
+ asChild,
808
+ showSeparator = false,
809
+ direction: directionProp,
810
+ className,
811
+ children,
812
+ style,
813
+ ...props
814
+ },
815
+ ref,
816
+ ) => {
817
+ const { width } = useWindowDimensions();
818
+ const insets = useSafeInsets();
819
+ const { placement: placementProp, variant } = useDrawerStyleContext();
820
+ const placement = resolveDrawerPlacement(placementProp);
821
+ const direction = directionProp ?? (width >= DRAWER_WIDE_BREAKPOINT ? 'row' : 'column');
822
+ const computedClassName = cn(drawerFooterVariants({ showSeparator, direction }), className);
823
+ const Comp = asChild ? Slot : View;
824
+ const paddingBottom =
825
+ variant === 'modal' && placement !== 'top'
826
+ ? Math.max(insets.bottom, DRAWER_EDGE_PADDING)
827
+ : DRAWER_EDGE_PADDING;
828
+
829
+ return (
830
+ <Comp
831
+ ref={ref}
832
+ className={computedClassName}
833
+ style={[{ paddingBottom }, style]}
834
+ {...props}
835
+ {...mergeDataAttributes(props, dataAttributes({ slot: 'drawer-footer' }))}
836
+ >
837
+ {children}
838
+ </Comp>
839
+ );
840
+ },
841
+ );
842
+
843
+ DrawerFooter.displayName = 'Drawer.Footer';
844
+
845
+ // =============================================================================
846
+ // COMPOUND COMPONENT
847
+ // =============================================================================
848
+
849
+ type DrawerCompoundComponent = typeof DrawerRoot & {
850
+ Header: typeof DrawerHeader;
851
+ LeadingSlot: typeof DrawerLeadingSlot;
852
+ Close: typeof DrawerClose;
853
+ Title: typeof DrawerTitle;
854
+ Description: typeof DrawerDescription;
855
+ Content: typeof DrawerContent;
856
+ Footer: typeof DrawerFooter;
857
+ };
858
+
859
+ export const Drawer = Object.assign(DrawerRoot, {
860
+ Header: DrawerHeader,
861
+ LeadingSlot: DrawerLeadingSlot,
862
+ Close: DrawerClose,
863
+ Title: DrawerTitle,
864
+ Description: DrawerDescription,
865
+ Content: DrawerContent,
866
+ Footer: DrawerFooter,
867
+ }) as DrawerCompoundComponent;
868
+
869
+ const internalStyles = StyleSheet.create({
870
+ right: {
871
+ position: 'absolute',
872
+ top: 0,
873
+ bottom: 0,
874
+ right: 0,
875
+ },
876
+ left: {
877
+ position: 'absolute',
878
+ top: 0,
879
+ bottom: 0,
880
+ left: 0,
881
+ },
882
+ top: {
883
+ position: 'absolute',
884
+ top: 0,
885
+ left: 0,
886
+ right: 0,
887
+ },
888
+ bottom: {
889
+ position: 'absolute',
890
+ bottom: 0,
891
+ left: 0,
892
+ right: 0,
893
+ },
894
+ });
895
+
896
+ export { MODAL_OVERLAY_Z_INDEX };