@seed-design/react-drawer 0.0.0-alpha-20260414104312

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.
@@ -0,0 +1,1361 @@
1
+ 'use client';
2
+ var jsxRuntime = require('react/jsx-runtime');
3
+ var reactComposeRefs = require('@radix-ui/react-compose-refs');
4
+ var reactFocusScope = require('@radix-ui/react-focus-scope');
5
+ var reactUseCallbackRef = require('@radix-ui/react-use-callback-ref');
6
+ var ariaHidden = require('aria-hidden');
7
+ var reactDismissibleLayer = require('@seed-design/react-dismissible-layer');
8
+ var reactPresence = require('@seed-design/react-presence');
9
+ var domUtils = require('@seed-design/dom-utils');
10
+ var reactPrimitive = require('@seed-design/react-primitive');
11
+ var React = require('react');
12
+ var reactUseControllableState = require('@seed-design/react-use-controllable-state');
13
+
14
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
15
+
16
+ var React__default = /*#__PURE__*/_interopDefault(React);
17
+
18
+ function isMobileFirefox() {
19
+ if (typeof window === "undefined" || typeof navigator === "undefined") return false;
20
+ return /Firefox/.test(navigator.userAgent) && /Mobile/.test(navigator.userAgent) || /FxiOS/.test(navigator.userAgent);
21
+ }
22
+ function isMac() {
23
+ return testPlatform(/^Mac/);
24
+ }
25
+ function isIPhone() {
26
+ return testPlatform(/^iPhone/);
27
+ }
28
+ function isSafari() {
29
+ return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
30
+ }
31
+ function isIPad() {
32
+ return testPlatform(/^iPad/) || // iPadOS 13 lies and says it's a Mac, but we can distinguish by detecting touch support.
33
+ isMac() && navigator.maxTouchPoints > 1;
34
+ }
35
+ function isIOS() {
36
+ return isIPhone() || isIPad();
37
+ }
38
+ function isAndroid() {
39
+ if (typeof window === "undefined" || typeof navigator === "undefined") return false;
40
+ return /Android/.test(navigator.userAgent);
41
+ }
42
+ // TODO: use userAgent instead?
43
+ function testPlatform(re) {
44
+ return typeof window !== "undefined" && window.navigator != null ? re.test(window.navigator.platform) : undefined;
45
+ }
46
+
47
+ /**
48
+ * TODO: move to recipe
49
+ */ const TRANSITIONS = {
50
+ ENTER_DURATION: 0.3,
51
+ EXIT_DURATION: 0.2,
52
+ OVERLAY_ENTER_TIMING_FUNCTION: "cubic-bezier(0, 0, 0.15, 1)",
53
+ OVERLAY_EXIT_TIMING_FUNCTION: "cubic-bezier(0.35, 0, 1, 1)",
54
+ CONTENT_ENTER_TIMING_FUNCTION: "cubic-bezier(0.03, 0.4, 0.1, 1)",
55
+ CONTENT_EXIT_TIMING_FUNCTION: "cubic-bezier(0.35, 0, 1, 1)"
56
+ };
57
+ const VELOCITY_THRESHOLD = 0.4;
58
+ const CLOSE_THRESHOLD = 0.25;
59
+ const SCROLL_LOCK_TIMEOUT = 100;
60
+ const WINDOW_TOP_OFFSET = 26;
61
+ const DRAG_CLASS = "seed-dragging";
62
+
63
+ const cache = new WeakMap();
64
+ // HTML input types that do not cause the software keyboard to appear.
65
+ const nonTextInputTypes = new Set([
66
+ "checkbox",
67
+ "radio",
68
+ "range",
69
+ "color",
70
+ "file",
71
+ "image",
72
+ "button",
73
+ "submit",
74
+ "reset"
75
+ ]);
76
+ function isInput(target) {
77
+ return target instanceof HTMLInputElement && !nonTextInputTypes.has(target.type) || target instanceof HTMLTextAreaElement || target instanceof HTMLElement && target.isContentEditable;
78
+ }
79
+ function set(el, styles, ignoreCache = false) {
80
+ if (!el || !(el instanceof HTMLElement)) return;
81
+ let originalStyles = {};
82
+ Object.entries(styles).forEach(([key, value])=>{
83
+ if (key.startsWith("--")) {
84
+ el.style.setProperty(key, value);
85
+ return;
86
+ }
87
+ originalStyles[key] = el.style[key];
88
+ el.style[key] = value;
89
+ });
90
+ if (ignoreCache) return;
91
+ cache.set(el, originalStyles);
92
+ }
93
+ function reset(el, prop) {
94
+ if (!el || !(el instanceof HTMLElement)) return;
95
+ let originalStyles = cache.get(el);
96
+ if (!originalStyles) {
97
+ return;
98
+ }
99
+ {
100
+ el.style[prop] = originalStyles[prop];
101
+ }
102
+ }
103
+ const isVertical = (direction)=>{
104
+ switch(direction){
105
+ case "top":
106
+ case "bottom":
107
+ return true;
108
+ case "left":
109
+ case "right":
110
+ return false;
111
+ default:
112
+ return direction;
113
+ }
114
+ };
115
+ function getTranslate(element, direction) {
116
+ if (!element) {
117
+ return null;
118
+ }
119
+ const style = window.getComputedStyle(element);
120
+ const transform = // @ts-ignore
121
+ style.transform || style.webkitTransform || style.mozTransform;
122
+ let mat = transform.match(/^matrix3d\((.+)\)$/);
123
+ if (mat) {
124
+ // https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/matrix3d
125
+ return Number.parseFloat(mat[1].split(", ")[isVertical(direction) ? 13 : 12]);
126
+ }
127
+ // https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/matrix
128
+ mat = transform.match(/^matrix\((.+)\)$/);
129
+ return mat ? Number.parseFloat(mat[1].split(", ")[isVertical(direction) ? 5 : 4]) : null;
130
+ }
131
+ function dampenValue(v) {
132
+ return 8 * (Math.log(v + 1) - 2);
133
+ }
134
+
135
+ // This code comes from https://github.com/emilkowalski/vaul/blob/main/src/use-position-fixed.ts
136
+ let previousBodyPosition = null;
137
+ /**
138
+ * This hook is necessary to prevent buggy behavior on iOS devices (need to test on Android).
139
+ * I won't get into too much detail about what bugs it solves, but so far I've found that setting the body to `position: fixed` is the most reliable way to prevent those bugs.
140
+ * Issues that this hook solves:
141
+ * https://github.com/emilkowalski/vaul/issues/435
142
+ * https://github.com/emilkowalski/vaul/issues/433
143
+ * And more that I discovered, but were just not reported.
144
+ */ function usePositionFixed({ isOpen, modal, nested, hasBeenOpened, preventScrollRestoration, noBodyStyles }) {
145
+ const [activeUrl, setActiveUrl] = React__default.default.useState(()=>typeof window !== "undefined" ? window.location.href : "");
146
+ const scrollPos = React__default.default.useRef(0);
147
+ const setPositionFixed = React__default.default.useCallback(()=>{
148
+ // All browsers on iOS will return true here.
149
+ if (!isSafari()) return;
150
+ // If previousBodyPosition is already set, don't set it again.
151
+ if (previousBodyPosition === null && isOpen && !noBodyStyles) {
152
+ previousBodyPosition = {
153
+ position: document.body.style.position,
154
+ top: document.body.style.top,
155
+ left: document.body.style.left,
156
+ height: document.body.style.height,
157
+ right: "unset"
158
+ };
159
+ // Update the dom inside an animation frame
160
+ const { scrollX, innerHeight } = window;
161
+ document.body.style.setProperty("position", "fixed", "important");
162
+ Object.assign(document.body.style, {
163
+ top: `${-scrollPos.current}px`,
164
+ left: `${-scrollX}px`,
165
+ right: "0px",
166
+ height: "auto"
167
+ });
168
+ window.setTimeout(()=>window.requestAnimationFrame(()=>{
169
+ // Attempt to check if the bottom bar appeared due to the position change
170
+ const bottomBarHeight = innerHeight - window.innerHeight;
171
+ if (bottomBarHeight && scrollPos.current >= innerHeight) {
172
+ // Move the content further up so that the bottom bar doesn't hide it
173
+ document.body.style.top = `${-(scrollPos.current + bottomBarHeight)}px`;
174
+ }
175
+ }), 300);
176
+ }
177
+ }, [
178
+ isOpen
179
+ ]);
180
+ const restorePositionSetting = React__default.default.useCallback(()=>{
181
+ // All browsers on iOS will return true here.
182
+ if (!isSafari()) return;
183
+ if (previousBodyPosition !== null && !noBodyStyles) {
184
+ // Convert the position from "px" to Int
185
+ const y = -parseInt(document.body.style.top, 10);
186
+ const x = -parseInt(document.body.style.left, 10);
187
+ // Restore styles
188
+ Object.assign(document.body.style, previousBodyPosition);
189
+ window.requestAnimationFrame(()=>{
190
+ if (preventScrollRestoration && activeUrl !== window.location.href) {
191
+ setActiveUrl(window.location.href);
192
+ return;
193
+ }
194
+ window.scrollTo(x, y);
195
+ });
196
+ previousBodyPosition = null;
197
+ }
198
+ }, [
199
+ activeUrl
200
+ ]);
201
+ React__default.default.useEffect(()=>{
202
+ function onScroll() {
203
+ scrollPos.current = window.scrollY;
204
+ }
205
+ onScroll();
206
+ window.addEventListener("scroll", onScroll);
207
+ return ()=>{
208
+ window.removeEventListener("scroll", onScroll);
209
+ };
210
+ }, []);
211
+ React__default.default.useEffect(()=>{
212
+ if (!modal) return;
213
+ return ()=>{
214
+ if (typeof document === "undefined") return;
215
+ // Another drawer is opened, safe to ignore the execution
216
+ const hasDrawerOpened = !!document.querySelector("[data-drawer]");
217
+ if (hasDrawerOpened) return;
218
+ restorePositionSetting();
219
+ };
220
+ }, [
221
+ modal,
222
+ restorePositionSetting
223
+ ]);
224
+ React__default.default.useEffect(()=>{
225
+ if (nested || !hasBeenOpened) return;
226
+ // This is needed to force Safari toolbar to show **before** the drawer starts animating to prevent a gnarly shift from happening
227
+ if (isOpen) {
228
+ // avoid for standalone mode (PWA)
229
+ const isStandalone = window.matchMedia("(display-mode: standalone)").matches;
230
+ !isStandalone && setPositionFixed();
231
+ if (!modal) {
232
+ window.setTimeout(()=>{
233
+ restorePositionSetting();
234
+ }, 500);
235
+ }
236
+ } else {
237
+ restorePositionSetting();
238
+ }
239
+ }, [
240
+ isOpen,
241
+ hasBeenOpened,
242
+ activeUrl,
243
+ modal,
244
+ nested,
245
+ setPositionFixed,
246
+ restorePositionSetting
247
+ ]);
248
+ return {
249
+ restorePositionSetting
250
+ };
251
+ }
252
+
253
+ function useSnapPoints({ activeSnapPointProp, setActiveSnapPointProp, snapPoints, drawerRef, overlayRef, fadeFromIndex, onSnapPointChange, direction = "bottom", container, snapToSequentialPoint }) {
254
+ const [activeSnapPoint, setActiveSnapPoint] = reactUseControllableState.useControllableState({
255
+ prop: activeSnapPointProp,
256
+ defaultProp: snapPoints?.[0] ?? null,
257
+ onChange: setActiveSnapPointProp
258
+ });
259
+ const [windowDimensions, setWindowDimensions] = React__default.default.useState(typeof window !== "undefined" ? {
260
+ innerWidth: window.innerWidth,
261
+ innerHeight: window.innerHeight
262
+ } : undefined);
263
+ React__default.default.useEffect(()=>{
264
+ function onResize() {
265
+ setWindowDimensions({
266
+ innerWidth: window.innerWidth,
267
+ innerHeight: window.innerHeight
268
+ });
269
+ }
270
+ window.addEventListener("resize", onResize);
271
+ return ()=>window.removeEventListener("resize", onResize);
272
+ }, []);
273
+ const isLastSnapPoint = React__default.default.useMemo(()=>activeSnapPoint === snapPoints?.[snapPoints.length - 1] || null, [
274
+ snapPoints,
275
+ activeSnapPoint
276
+ ]);
277
+ const activeSnapPointIndex = React__default.default.useMemo(()=>snapPoints?.findIndex((snapPoint)=>snapPoint === activeSnapPoint) ?? null, [
278
+ snapPoints,
279
+ activeSnapPoint
280
+ ]);
281
+ const shouldFade = snapPoints && snapPoints.length > 0 && (fadeFromIndex || fadeFromIndex === 0) && !Number.isNaN(fadeFromIndex) && snapPoints[fadeFromIndex] === activeSnapPoint || !snapPoints;
282
+ const snapPointsOffset = React__default.default.useMemo(()=>{
283
+ const containerSize = container ? {
284
+ width: container.getBoundingClientRect().width,
285
+ height: container.getBoundingClientRect().height
286
+ } : typeof window !== "undefined" ? {
287
+ width: window.innerWidth,
288
+ height: window.innerHeight
289
+ } : {
290
+ width: 0,
291
+ height: 0
292
+ };
293
+ return snapPoints?.map((snapPoint)=>{
294
+ // FIXME
295
+ // 1 -> container 100% << expected
296
+ // 0.5 -> container 50% << expected
297
+ // 300px -> 300 -> 300px << expected
298
+ // 15rem -> 15 -> 15px << this makes no sense, should fix or disallow
299
+ const isPx = typeof snapPoint === "string";
300
+ let snapPointAsNumber = 0;
301
+ if (isPx) {
302
+ snapPointAsNumber = Number.parseInt(snapPoint, 10);
303
+ }
304
+ if (isVertical(direction)) {
305
+ const height = isPx ? snapPointAsNumber : windowDimensions ? snapPoint * containerSize.height : 0;
306
+ if (windowDimensions) {
307
+ return direction === "bottom" ? containerSize.height - height : -containerSize.height + height;
308
+ }
309
+ return height;
310
+ }
311
+ const width = isPx ? snapPointAsNumber : windowDimensions ? snapPoint * containerSize.width : 0;
312
+ if (windowDimensions) {
313
+ return direction === "right" ? containerSize.width - width : -containerSize.width + width;
314
+ }
315
+ return width;
316
+ }) ?? [];
317
+ }, [
318
+ snapPoints,
319
+ windowDimensions,
320
+ container,
321
+ direction
322
+ ]);
323
+ const activeSnapPointOffset = React__default.default.useMemo(()=>activeSnapPointIndex !== null ? snapPointsOffset?.[activeSnapPointIndex] : null, [
324
+ snapPointsOffset,
325
+ activeSnapPointIndex
326
+ ]);
327
+ const snapToPoint = React__default.default.useCallback((dimension)=>{
328
+ const newSnapPointIndex = snapPointsOffset?.findIndex((snapPointDim)=>snapPointDim === dimension) ?? null;
329
+ onSnapPointChange(newSnapPointIndex);
330
+ set(drawerRef.current, {
331
+ transition: `transform ${TRANSITIONS.ENTER_DURATION}s ${TRANSITIONS.CONTENT_ENTER_TIMING_FUNCTION}`,
332
+ transform: isVertical(direction) ? `translate3d(0, ${dimension}px, 0)` : `translate3d(${dimension}px, 0, 0)`
333
+ });
334
+ if (snapPointsOffset && newSnapPointIndex !== snapPointsOffset.length - 1 && fadeFromIndex !== undefined && newSnapPointIndex !== fadeFromIndex && newSnapPointIndex < fadeFromIndex) {
335
+ if (fadeFromIndex !== 0) {
336
+ set(overlayRef.current, {
337
+ transition: `opacity ${TRANSITIONS.EXIT_DURATION}s ${TRANSITIONS.OVERLAY_EXIT_TIMING_FUNCTION}`,
338
+ opacity: "0"
339
+ });
340
+ }
341
+ } else {
342
+ set(overlayRef.current, {
343
+ transition: `opacity ${TRANSITIONS.ENTER_DURATION}s ${TRANSITIONS.OVERLAY_ENTER_TIMING_FUNCTION}`,
344
+ opacity: "1"
345
+ });
346
+ }
347
+ setActiveSnapPoint(snapPoints?.[Math.max(newSnapPointIndex, 0)]);
348
+ }, [
349
+ drawerRef,
350
+ overlayRef,
351
+ snapPoints,
352
+ snapPointsOffset,
353
+ fadeFromIndex,
354
+ direction,
355
+ onSnapPointChange,
356
+ setActiveSnapPoint
357
+ ]);
358
+ React__default.default.useEffect(()=>{
359
+ if (activeSnapPoint || activeSnapPointProp) {
360
+ const newIndex = snapPoints?.findIndex((snapPoint)=>snapPoint === activeSnapPointProp || snapPoint === activeSnapPoint) ?? -1;
361
+ if (snapPointsOffset && newIndex !== -1 && typeof snapPointsOffset[newIndex] === "number") {
362
+ snapToPoint(snapPointsOffset[newIndex]);
363
+ }
364
+ }
365
+ }, [
366
+ activeSnapPoint,
367
+ activeSnapPointProp,
368
+ snapPoints,
369
+ snapPointsOffset,
370
+ snapToPoint
371
+ ]);
372
+ function onRelease({ draggedDistance, closeDrawer, velocity, dismissible, event }) {
373
+ if (fadeFromIndex === undefined) return;
374
+ const currentPosition = direction === "bottom" || direction === "right" ? (activeSnapPointOffset ?? 0) - draggedDistance : (activeSnapPointOffset ?? 0) + draggedDistance;
375
+ const isOverlaySnapPoint = activeSnapPointIndex === fadeFromIndex - 1;
376
+ const isFirst = activeSnapPointIndex === 0;
377
+ const hasDraggedUp = draggedDistance > 0;
378
+ if (isOverlaySnapPoint) {
379
+ set(overlayRef.current, {
380
+ transition: `opacity ${TRANSITIONS.EXIT_DURATION}s ${TRANSITIONS.OVERLAY_EXIT_TIMING_FUNCTION}`
381
+ });
382
+ }
383
+ if (!snapToSequentialPoint && velocity > 2 && !hasDraggedUp) {
384
+ if (dismissible) closeDrawer(false, {
385
+ reason: "drag",
386
+ event
387
+ });
388
+ else snapToPoint(snapPointsOffset[0]); // snap to initial point
389
+ return;
390
+ }
391
+ if (!snapToSequentialPoint && velocity > 2 && hasDraggedUp && snapPointsOffset && snapPoints) {
392
+ snapToPoint(snapPointsOffset[snapPoints.length - 1]);
393
+ return;
394
+ }
395
+ // Find the closest snap point to the current position
396
+ const closestSnapPoint = snapPointsOffset?.reduce((prev, curr)=>{
397
+ if (typeof prev !== "number" || typeof curr !== "number") return prev;
398
+ return Math.abs(curr - currentPosition) < Math.abs(prev - currentPosition) ? curr : prev;
399
+ });
400
+ const dim = isVertical(direction) ? window.innerHeight : window.innerWidth;
401
+ if (velocity > VELOCITY_THRESHOLD && Math.abs(draggedDistance) < dim * 0.4) {
402
+ const dragDirection = hasDraggedUp ? 1 : -1; // 1 = up, -1 = down
403
+ // Don't do anything if we swipe upwards while being on the last snap point
404
+ if (dragDirection > 0 && isLastSnapPoint && snapPoints) {
405
+ snapToPoint(snapPointsOffset[snapPoints.length - 1]);
406
+ return;
407
+ }
408
+ if (isFirst && dragDirection < 0 && dismissible) {
409
+ closeDrawer(false, {
410
+ reason: "drag",
411
+ event
412
+ });
413
+ }
414
+ if (activeSnapPointIndex === null) return;
415
+ snapToPoint(snapPointsOffset[activeSnapPointIndex + dragDirection]);
416
+ return;
417
+ }
418
+ snapToPoint(closestSnapPoint);
419
+ }
420
+ function onDrag({ draggedDistance }) {
421
+ if (activeSnapPointOffset === null) return;
422
+ const newValue = direction === "bottom" || direction === "right" ? activeSnapPointOffset - draggedDistance : activeSnapPointOffset + draggedDistance;
423
+ // Don't do anything if we exceed the last(biggest) snap point
424
+ if ((direction === "bottom" || direction === "right") && newValue < snapPointsOffset[snapPointsOffset.length - 1]) {
425
+ return;
426
+ }
427
+ if ((direction === "top" || direction === "left") && newValue > snapPointsOffset[snapPointsOffset.length - 1]) {
428
+ return;
429
+ }
430
+ set(drawerRef.current, {
431
+ transform: isVertical(direction) ? `translate3d(0, ${newValue}px, 0)` : `translate3d(${newValue}px, 0, 0)`
432
+ });
433
+ }
434
+ function getPercentageDragged(absDraggedDistance, isDraggingDown) {
435
+ if (!snapPoints || typeof activeSnapPointIndex !== "number" || !snapPointsOffset || fadeFromIndex === undefined) return null;
436
+ // If this is true we are dragging to a snap point that is supposed to have an overlay
437
+ const isOverlaySnapPoint = activeSnapPointIndex === fadeFromIndex - 1;
438
+ const isOverlaySnapPointOrHigher = activeSnapPointIndex >= fadeFromIndex;
439
+ if (isOverlaySnapPointOrHigher && isDraggingDown) {
440
+ return 0;
441
+ }
442
+ // Don't animate, but still use this one if we are dragging away from the overlaySnapPoint
443
+ if (isOverlaySnapPoint && !isDraggingDown) return 1;
444
+ if (!shouldFade && !isOverlaySnapPoint) return null;
445
+ // Either fadeFrom index or the one before
446
+ const targetSnapPointIndex = isOverlaySnapPoint ? activeSnapPointIndex + 1 : activeSnapPointIndex - 1;
447
+ if (fadeFromIndex === 0 && activeSnapPointIndex === 0) {
448
+ let firstSnapPoint = snapPoints[0];
449
+ if (typeof firstSnapPoint === "string") {
450
+ firstSnapPoint = Number.parseInt(firstSnapPoint, 10);
451
+ }
452
+ const snapPointDistance = firstSnapPoint;
453
+ const percentageDragged = absDraggedDistance / Math.abs(snapPointDistance);
454
+ return percentageDragged;
455
+ }
456
+ // Get the distance from overlaySnapPoint to the one before or vice-versa to calculate the opacity percentage accordingly
457
+ const snapPointDistance = isOverlaySnapPoint ? snapPointsOffset[targetSnapPointIndex] - snapPointsOffset[targetSnapPointIndex - 1] : snapPointsOffset[targetSnapPointIndex + 1] - snapPointsOffset[targetSnapPointIndex];
458
+ const percentageDragged = absDraggedDistance / Math.abs(snapPointDistance);
459
+ if (isOverlaySnapPoint) {
460
+ return 1 - percentageDragged;
461
+ }
462
+ return percentageDragged;
463
+ }
464
+ return {
465
+ isLastSnapPoint,
466
+ activeSnapPoint,
467
+ shouldFade,
468
+ getPercentageDragged,
469
+ setActiveSnapPoint,
470
+ activeSnapPointIndex,
471
+ onRelease,
472
+ onDrag,
473
+ snapPointsOffset
474
+ };
475
+ }
476
+
477
+ function useDrawer(props) {
478
+ const { open: openProp, onOpenChange, onDrag: onDragProp, onRelease: onReleaseProp, snapPoints, closeThreshold = CLOSE_THRESHOLD, scrollLockTimeout = SCROLL_LOCK_TIMEOUT, dismissible = true, handleOnly = false, fadeFromIndex = snapPoints && snapPoints.length - 1, activeSnapPoint: activeSnapPointProp, setActiveSnapPoint: setActiveSnapPointProp, fixed, modal = true, onClose, nested, noBodyStyles = true, direction = "bottom", defaultOpen = false, snapToSequentialPoint = false, preventScrollRestoration = false, repositionInputs = true, onAnimationEnd, container, autoFocus = true, closeOnInteractOutside = true, closeOnEscape = true, lazyMount: lazyMountProp = false, unmountOnExit: unmountOnExitProp = false } = props;
479
+ const drawerId = React.useId();
480
+ const titleId = `${drawerId}-title`;
481
+ const descriptionId = `${drawerId}-description`;
482
+ const [isOpen = false, setIsOpen] = reactUseControllableState.useControllableState({
483
+ defaultProp: defaultOpen,
484
+ prop: openProp,
485
+ onChange: (o, details)=>{
486
+ onOpenChange?.(o, details);
487
+ if (!o && !nested) {
488
+ restorePositionSetting();
489
+ }
490
+ setTimeout(()=>{
491
+ onAnimationEnd?.(o);
492
+ }, TRANSITIONS.EXIT_DURATION * 1000);
493
+ if (o && !modal) {
494
+ if (typeof window !== "undefined") {
495
+ window.requestAnimationFrame(()=>{
496
+ document.body.style.pointerEvents = "auto";
497
+ });
498
+ }
499
+ }
500
+ if (!o) {
501
+ document.body.style.pointerEvents = "auto";
502
+ }
503
+ }
504
+ });
505
+ const [hasBeenOpened, setHasBeenOpened] = React.useState(false);
506
+ const [hasAnimationDone, setHasAnimationDone] = React.useState(false);
507
+ const [isDragging, setIsDragging] = React.useState(false);
508
+ const [shouldOverlayAnimate, setShouldOverlayAnimate] = React.useState(false);
509
+ const [isCloseButtonRendered, setIsCloseButtonRendered] = React.useState(false);
510
+ const closeButtonRef = React.useCallback((node)=>{
511
+ setIsCloseButtonRendered(!!node);
512
+ }, []);
513
+ const overlayRef = React.useRef(null);
514
+ const openTime = React.useRef(null);
515
+ const dragStartTime = React.useRef(null);
516
+ const dragEndTime = React.useRef(null);
517
+ const lastTimeDragPrevented = React.useRef(null);
518
+ const isAllowedToDrag = React.useRef(false);
519
+ const pointerStart = React.useRef(0);
520
+ const keyboardIsOpen = React.useRef(false);
521
+ const previousDiffFromInitial = React.useRef(0);
522
+ const drawerRef = React.useRef(null);
523
+ const drawerHeightRef = React.useRef(drawerRef.current?.getBoundingClientRect().height || 0);
524
+ const drawerWidthRef = React.useRef(drawerRef.current?.getBoundingClientRect().width || 0);
525
+ const initialDrawerHeight = React.useRef(0);
526
+ const onSnapPointChange = React.useCallback((activeSnapPointIndex)=>{
527
+ if (snapPoints && activeSnapPointIndex === snapPointsOffset.length - 1) {
528
+ openTime.current = new Date();
529
+ }
530
+ }, [
531
+ snapPoints
532
+ ]);
533
+ const { activeSnapPoint, activeSnapPointIndex, setActiveSnapPoint, onRelease: onReleaseSnapPoints, snapPointsOffset, onDrag: onDragSnapPoints, shouldFade, getPercentageDragged: getSnapPointsPercentageDragged } = useSnapPoints({
534
+ snapPoints,
535
+ activeSnapPointProp,
536
+ setActiveSnapPointProp,
537
+ drawerRef,
538
+ fadeFromIndex,
539
+ overlayRef,
540
+ onSnapPointChange,
541
+ direction,
542
+ snapToSequentialPoint
543
+ });
544
+ const { restorePositionSetting } = usePositionFixed({
545
+ isOpen,
546
+ modal,
547
+ nested: nested ?? false,
548
+ hasBeenOpened,
549
+ preventScrollRestoration,
550
+ noBodyStyles
551
+ });
552
+ function onPress(event) {
553
+ if (!dismissible && !snapPoints) return;
554
+ if (drawerRef.current && !drawerRef.current.contains(event.target)) return;
555
+ drawerHeightRef.current = drawerRef.current?.getBoundingClientRect().height || 0;
556
+ drawerWidthRef.current = drawerRef.current?.getBoundingClientRect().width || 0;
557
+ setIsDragging(true);
558
+ dragStartTime.current = new Date();
559
+ if (isIOS()) {
560
+ window.addEventListener("touchend", ()=>{
561
+ isAllowedToDrag.current = false;
562
+ }, {
563
+ once: true
564
+ });
565
+ }
566
+ event.target.setPointerCapture(event.pointerId);
567
+ pointerStart.current = isVertical(direction) ? event.pageY : event.pageX;
568
+ }
569
+ function shouldDrag(el, isDraggingInDirection) {
570
+ let element = el;
571
+ const highlightedText = window.getSelection()?.toString();
572
+ const swipeAmount = drawerRef.current ? getTranslate(drawerRef.current, direction) : null;
573
+ const date = new Date();
574
+ if (element.tagName === "SELECT") {
575
+ return false;
576
+ }
577
+ if (element.hasAttribute("data-no-drag") || element.closest("[data-no-drag]")) {
578
+ return false;
579
+ }
580
+ if (direction === "right" || direction === "left") {
581
+ return true;
582
+ }
583
+ if (openTime.current && date.getTime() - openTime.current.getTime() < 500) {
584
+ return false;
585
+ }
586
+ if (swipeAmount !== null) {
587
+ if (direction === "bottom" ? swipeAmount > 0 : swipeAmount < 0) {
588
+ return true;
589
+ }
590
+ }
591
+ if (highlightedText && highlightedText.length > 0) {
592
+ return false;
593
+ }
594
+ if (lastTimeDragPrevented.current && date.getTime() - lastTimeDragPrevented.current.getTime() < scrollLockTimeout && swipeAmount === 0) {
595
+ lastTimeDragPrevented.current = date;
596
+ return false;
597
+ }
598
+ if (isDraggingInDirection) {
599
+ lastTimeDragPrevented.current = date;
600
+ return false;
601
+ }
602
+ while(element){
603
+ if (element.scrollHeight > element.clientHeight) {
604
+ if (element.scrollTop !== 0) {
605
+ lastTimeDragPrevented.current = new Date();
606
+ return false;
607
+ }
608
+ if (element.getAttribute("role") === "dialog") {
609
+ return true;
610
+ }
611
+ }
612
+ element = element.parentNode;
613
+ }
614
+ return true;
615
+ }
616
+ function onDrag(event) {
617
+ if (!drawerRef.current) return;
618
+ if (isDragging) {
619
+ const directionMultiplier = direction === "bottom" || direction === "right" ? 1 : -1;
620
+ const draggedDistance = (pointerStart.current - (isVertical(direction) ? event.pageY : event.pageX)) * directionMultiplier;
621
+ const isDraggingInDirection = draggedDistance > 0;
622
+ const noCloseSnapPointsPreCondition = snapPoints && !dismissible && !isDraggingInDirection;
623
+ if (noCloseSnapPointsPreCondition && activeSnapPointIndex === 0) return;
624
+ const absDraggedDistance = Math.abs(draggedDistance);
625
+ const drawerDimension = direction === "bottom" || direction === "top" ? drawerHeightRef.current : drawerWidthRef.current;
626
+ let percentageDragged = absDraggedDistance / drawerDimension;
627
+ const snapPointPercentageDragged = getSnapPointsPercentageDragged(absDraggedDistance, isDraggingInDirection);
628
+ if (snapPointPercentageDragged !== null) {
629
+ percentageDragged = snapPointPercentageDragged;
630
+ }
631
+ if (noCloseSnapPointsPreCondition && percentageDragged >= 1) {
632
+ return;
633
+ }
634
+ if (!isAllowedToDrag.current && !shouldDrag(event.target, isDraggingInDirection)) return;
635
+ drawerRef.current.classList.add(DRAG_CLASS);
636
+ isAllowedToDrag.current = true;
637
+ set(drawerRef.current, {
638
+ transition: "none"
639
+ });
640
+ set(overlayRef.current, {
641
+ transition: "none"
642
+ });
643
+ if (snapPoints) {
644
+ onDragSnapPoints({
645
+ draggedDistance
646
+ });
647
+ }
648
+ if (isDraggingInDirection && !snapPoints) {
649
+ const dampenedDraggedDistance = dampenValue(draggedDistance);
650
+ const translateValue = Math.min(dampenedDraggedDistance * -1, 0) * directionMultiplier;
651
+ set(drawerRef.current, {
652
+ transform: isVertical(direction) ? `translate3d(0, ${translateValue}px, 0)` : `translate3d(${translateValue}px, 0, 0)`
653
+ });
654
+ return;
655
+ }
656
+ const opacityValue = 1 - percentageDragged;
657
+ if (shouldFade || fadeFromIndex && activeSnapPointIndex === fadeFromIndex - 1) {
658
+ onDragProp?.(event, percentageDragged);
659
+ set(overlayRef.current, {
660
+ opacity: `${opacityValue}`,
661
+ transition: "none"
662
+ }, true);
663
+ }
664
+ if (!snapPoints) {
665
+ const translateValue = absDraggedDistance * directionMultiplier;
666
+ set(drawerRef.current, {
667
+ transform: isVertical(direction) ? `translate3d(0, ${translateValue}px, 0)` : `translate3d(${translateValue}px, 0, 0)`
668
+ });
669
+ }
670
+ }
671
+ }
672
+ const cancelDrag = React.useCallback(()=>{
673
+ if (!isDragging || !drawerRef.current) return;
674
+ drawerRef.current.classList.remove(DRAG_CLASS);
675
+ isAllowedToDrag.current = false;
676
+ setIsDragging(false);
677
+ dragEndTime.current = new Date();
678
+ }, [
679
+ isDragging
680
+ ]);
681
+ const closeDrawer = React.useCallback((fromWithin, details)=>{
682
+ cancelDrag();
683
+ onClose?.();
684
+ if (!fromWithin) {
685
+ setIsOpen(false, details);
686
+ }
687
+ if (fadeFromIndex !== undefined && fadeFromIndex > 0 && activeSnapPointIndex === 0) {
688
+ set(overlayRef.current, {
689
+ opacity: "0"
690
+ });
691
+ }
692
+ setTimeout(()=>{
693
+ if (snapPoints) {
694
+ setActiveSnapPoint(snapPoints[0]);
695
+ }
696
+ }, TRANSITIONS.EXIT_DURATION * 1000);
697
+ }, [
698
+ cancelDrag,
699
+ onClose,
700
+ snapPoints,
701
+ setActiveSnapPoint,
702
+ setIsOpen,
703
+ fadeFromIndex,
704
+ activeSnapPointIndex
705
+ ]);
706
+ function resetDrawer() {
707
+ if (!drawerRef.current) return;
708
+ set(drawerRef.current, {
709
+ transform: "translate3d(0, 0, 0)",
710
+ transition: `transform ${TRANSITIONS.EXIT_DURATION}s ${TRANSITIONS.CONTENT_EXIT_TIMING_FUNCTION}`
711
+ });
712
+ set(overlayRef.current, {
713
+ transition: `opacity ${TRANSITIONS.EXIT_DURATION}s ${TRANSITIONS.OVERLAY_EXIT_TIMING_FUNCTION}`,
714
+ opacity: "1"
715
+ });
716
+ }
717
+ function onRelease(event) {
718
+ if (!isDragging || !drawerRef.current) return;
719
+ drawerRef.current.classList.remove(DRAG_CLASS);
720
+ isAllowedToDrag.current = false;
721
+ setIsDragging(false);
722
+ dragEndTime.current = new Date();
723
+ const swipeAmount = getTranslate(drawerRef.current, direction);
724
+ if (!event || !shouldDrag(event.target, false) || !swipeAmount || Number.isNaN(swipeAmount)) return;
725
+ if (dragStartTime.current === null) return;
726
+ const timeTaken = dragEndTime.current.getTime() - dragStartTime.current.getTime();
727
+ const distMoved = pointerStart.current - (isVertical(direction) ? event.pageY : event.pageX);
728
+ const velocity = Math.abs(distMoved) / timeTaken;
729
+ if (snapPoints) {
730
+ const directionMultiplier = direction === "bottom" || direction === "right" ? 1 : -1;
731
+ onReleaseSnapPoints({
732
+ draggedDistance: distMoved * directionMultiplier,
733
+ closeDrawer,
734
+ velocity,
735
+ dismissible,
736
+ event: event.nativeEvent
737
+ });
738
+ onReleaseProp?.(event, true);
739
+ return;
740
+ }
741
+ if (direction === "bottom" || direction === "right" ? distMoved > 0 : distMoved < 0) {
742
+ resetDrawer();
743
+ onReleaseProp?.(event, true);
744
+ return;
745
+ }
746
+ if (velocity > VELOCITY_THRESHOLD) {
747
+ closeDrawer(false, {
748
+ reason: "drag",
749
+ event: event.nativeEvent
750
+ });
751
+ onReleaseProp?.(event, false);
752
+ return;
753
+ }
754
+ const visibleDrawerHeight = Math.min(drawerRef.current.getBoundingClientRect().height ?? 0, window.innerHeight);
755
+ const visibleDrawerWidth = Math.min(drawerRef.current.getBoundingClientRect().width ?? 0, window.innerWidth);
756
+ const isHorizontalSwipe = direction === "left" || direction === "right";
757
+ if (Math.abs(swipeAmount) >= (isHorizontalSwipe ? visibleDrawerWidth : visibleDrawerHeight) * closeThreshold) {
758
+ closeDrawer(false, {
759
+ reason: "drag",
760
+ event: event.nativeEvent
761
+ });
762
+ onReleaseProp?.(event, false);
763
+ return;
764
+ }
765
+ onReleaseProp?.(event, true);
766
+ resetDrawer();
767
+ }
768
+ React.useEffect(()=>{
769
+ if (isOpen) {
770
+ set(document.documentElement, {
771
+ scrollBehavior: "auto"
772
+ });
773
+ openTime.current = new Date();
774
+ }
775
+ return ()=>{
776
+ reset(document.documentElement, "scrollBehavior");
777
+ };
778
+ }, [
779
+ isOpen
780
+ ]);
781
+ React.useEffect(()=>{
782
+ function onVisualViewportChange() {
783
+ if (!drawerRef.current || !repositionInputs) return;
784
+ const focusedElement = document.activeElement;
785
+ if (isInput(focusedElement) || keyboardIsOpen.current) {
786
+ const visualViewportHeight = window.visualViewport?.height || 0;
787
+ const totalHeight = window.innerHeight;
788
+ let diffFromInitial = totalHeight - visualViewportHeight;
789
+ const drawerHeight = drawerRef.current.getBoundingClientRect().height || 0;
790
+ const isTallEnough = drawerHeight > totalHeight * 0.8;
791
+ if (!initialDrawerHeight.current) {
792
+ initialDrawerHeight.current = drawerHeight;
793
+ }
794
+ const offsetFromTop = drawerRef.current.getBoundingClientRect().top;
795
+ if (Math.abs(previousDiffFromInitial.current - diffFromInitial) > 60) {
796
+ keyboardIsOpen.current = !keyboardIsOpen.current;
797
+ }
798
+ if (snapPoints && snapPoints.length > 0 && snapPointsOffset && activeSnapPointIndex) {
799
+ const activeSnapPointHeight = snapPointsOffset[activeSnapPointIndex] || 0;
800
+ diffFromInitial += activeSnapPointHeight;
801
+ }
802
+ previousDiffFromInitial.current = diffFromInitial;
803
+ if (drawerHeight > visualViewportHeight || keyboardIsOpen.current) {
804
+ const height = drawerRef.current.getBoundingClientRect().height;
805
+ let newDrawerHeight = height;
806
+ if (height > visualViewportHeight) {
807
+ newDrawerHeight = visualViewportHeight - (isTallEnough ? offsetFromTop : WINDOW_TOP_OFFSET);
808
+ }
809
+ if (fixed) {
810
+ drawerRef.current.style.height = `${height - Math.max(diffFromInitial, 0)}px`;
811
+ } else {
812
+ drawerRef.current.style.height = `${Math.max(newDrawerHeight, visualViewportHeight - offsetFromTop)}px`;
813
+ }
814
+ } else if (!isMobileFirefox() && !isAndroid()) {
815
+ drawerRef.current.style.height = `${initialDrawerHeight.current}px`;
816
+ }
817
+ if (snapPoints && snapPoints.length > 0 && !keyboardIsOpen.current) {
818
+ drawerRef.current.style.bottom = "0px";
819
+ } else {
820
+ drawerRef.current.style.bottom = `${Math.max(diffFromInitial, 0)}px`;
821
+ }
822
+ }
823
+ }
824
+ window.visualViewport?.addEventListener("resize", onVisualViewportChange);
825
+ return ()=>window.visualViewport?.removeEventListener("resize", onVisualViewportChange);
826
+ }, [
827
+ activeSnapPointIndex,
828
+ snapPoints,
829
+ snapPointsOffset,
830
+ repositionInputs,
831
+ fixed
832
+ ]);
833
+ React.useEffect(()=>{
834
+ if (!modal) {
835
+ window.requestAnimationFrame(()=>{
836
+ document.body.style.pointerEvents = "auto";
837
+ });
838
+ }
839
+ }, [
840
+ modal
841
+ ]);
842
+ // Effect 1: Track drawer open state
843
+ React.useEffect(()=>{
844
+ if (isOpen) {
845
+ setHasBeenOpened(true);
846
+ }
847
+ }, [
848
+ isOpen
849
+ ]);
850
+ // Effect 2: Handle animation state and timer
851
+ React.useEffect(()=>{
852
+ if (isOpen) {
853
+ // Only reset animation state if this is the first open
854
+ if (!hasBeenOpened) {
855
+ setHasAnimationDone(false);
856
+ }
857
+ const timeoutId = setTimeout(()=>{
858
+ setHasAnimationDone(true);
859
+ }, TRANSITIONS.ENTER_DURATION * 1000);
860
+ return ()=>clearTimeout(timeoutId);
861
+ }
862
+ // Reset animation state when drawer closes
863
+ setHasAnimationDone(false);
864
+ }, [
865
+ isOpen,
866
+ hasBeenOpened
867
+ ]);
868
+ React.useEffect(()=>{
869
+ if (isOpen && snapPoints && fadeFromIndex === 0) {
870
+ setShouldOverlayAnimate(true);
871
+ const timeoutId = setTimeout(()=>{
872
+ setShouldOverlayAnimate(false);
873
+ }, TRANSITIONS.ENTER_DURATION * 1000);
874
+ return ()=>clearTimeout(timeoutId);
875
+ }
876
+ setShouldOverlayAnimate(false);
877
+ }, [
878
+ isOpen,
879
+ snapPoints,
880
+ fadeFromIndex
881
+ ]);
882
+ const stateProps = React.useMemo(()=>domUtils.elementProps({
883
+ "data-open": domUtils.dataAttr(isOpen)
884
+ }), [
885
+ isOpen
886
+ ]);
887
+ return React.useMemo(()=>({
888
+ activeSnapPoint,
889
+ snapPoints,
890
+ setActiveSnapPoint,
891
+ drawerRef,
892
+ overlayRef,
893
+ shouldOverlayAnimate,
894
+ onOpenChange,
895
+ onPress,
896
+ onRelease,
897
+ onDrag,
898
+ dismissible,
899
+ handleOnly,
900
+ isOpen,
901
+ isDragging,
902
+ shouldFade,
903
+ closeDrawer,
904
+ keyboardIsOpen,
905
+ modal,
906
+ snapPointsOffset,
907
+ activeSnapPointIndex,
908
+ direction,
909
+ noBodyStyles,
910
+ container,
911
+ autoFocus,
912
+ setHasBeenOpened,
913
+ setIsOpen,
914
+ closeOnInteractOutside,
915
+ closeOnEscape,
916
+ titleId,
917
+ descriptionId,
918
+ lazyMount: lazyMountProp,
919
+ unmountOnExit: unmountOnExitProp,
920
+ hasAnimationDone,
921
+ closeButtonRef,
922
+ isCloseButtonRendered,
923
+ triggerProps: domUtils.buttonProps({
924
+ ...stateProps,
925
+ onClick: (e)=>{
926
+ if (e.defaultPrevented) return;
927
+ setIsOpen(true);
928
+ }
929
+ }),
930
+ positionerProps: domUtils.elementProps({
931
+ ...stateProps,
932
+ style: {
933
+ pointerEvents: isOpen && modal ? undefined : "none"
934
+ }
935
+ }),
936
+ backdropProps: domUtils.elementProps({
937
+ ...stateProps
938
+ }),
939
+ titleProps: domUtils.elementProps({
940
+ id: titleId,
941
+ ...stateProps
942
+ }),
943
+ descriptionProps: domUtils.elementProps({
944
+ id: descriptionId,
945
+ ...stateProps
946
+ }),
947
+ headerProps: domUtils.elementProps({
948
+ "data-show-close-button": domUtils.dataAttr(isCloseButtonRendered),
949
+ ...stateProps
950
+ }),
951
+ closeButtonProps: domUtils.buttonProps({
952
+ ...stateProps,
953
+ onClick: (e)=>{
954
+ if (e.defaultPrevented) return;
955
+ setIsOpen(false, {
956
+ reason: "closeButton",
957
+ event: e.nativeEvent
958
+ });
959
+ }
960
+ })
961
+ }), [
962
+ activeSnapPoint,
963
+ snapPoints,
964
+ setActiveSnapPoint,
965
+ onOpenChange,
966
+ dismissible,
967
+ handleOnly,
968
+ isOpen,
969
+ isDragging,
970
+ shouldFade,
971
+ shouldOverlayAnimate,
972
+ closeDrawer,
973
+ modal,
974
+ snapPointsOffset,
975
+ activeSnapPointIndex,
976
+ direction,
977
+ noBodyStyles,
978
+ container,
979
+ autoFocus,
980
+ setIsOpen,
981
+ closeOnInteractOutside,
982
+ closeOnEscape,
983
+ onRelease,
984
+ onDrag,
985
+ onPress,
986
+ titleId,
987
+ descriptionId,
988
+ lazyMountProp,
989
+ unmountOnExitProp,
990
+ hasAnimationDone,
991
+ closeButtonRef,
992
+ isCloseButtonRendered,
993
+ stateProps
994
+ ]);
995
+ }
996
+
997
+ const DrawerContext = /*#__PURE__*/ React.createContext(null);
998
+ const DrawerProvider = DrawerContext.Provider;
999
+ function useDrawerContext() {
1000
+ const context = React.useContext(DrawerContext);
1001
+ if (context === null) {
1002
+ throw new Error("useDrawerContext must be used within DrawerProvider");
1003
+ }
1004
+ return context;
1005
+ }
1006
+
1007
+ const DrawerRoot = (props)=>{
1008
+ const api = useDrawer(props);
1009
+ return /*#__PURE__*/ jsxRuntime.jsx(DrawerProvider, {
1010
+ value: api,
1011
+ children: props.children
1012
+ });
1013
+ };
1014
+ const DrawerTrigger = /*#__PURE__*/ React.forwardRef((props, ref)=>{
1015
+ const api = useDrawerContext();
1016
+ return /*#__PURE__*/ jsxRuntime.jsx(reactPrimitive.Primitive.button, {
1017
+ ref: ref,
1018
+ ...domUtils.mergeProps(api.triggerProps, props)
1019
+ });
1020
+ });
1021
+ DrawerTrigger.displayName = "DrawerTrigger";
1022
+ const DrawerPositioner = /*#__PURE__*/ React.forwardRef((props, ref)=>{
1023
+ const api = useDrawerContext();
1024
+ return /*#__PURE__*/ jsxRuntime.jsx(reactPrimitive.Primitive.div, {
1025
+ ref: ref,
1026
+ ...domUtils.mergeProps(api.positionerProps, props)
1027
+ });
1028
+ });
1029
+ DrawerPositioner.displayName = "DrawerPositioner";
1030
+ const DrawerBackdrop = /*#__PURE__*/ React.forwardRef((props, ref)=>{
1031
+ const { overlayRef, onRelease, modal, snapPoints, isOpen, shouldFade, shouldOverlayAnimate, hasAnimationDone, lazyMount, unmountOnExit } = useDrawerContext();
1032
+ const composedRef = reactComposeRefs.composeRefs(ref, overlayRef);
1033
+ const hasSnapPoints = snapPoints && snapPoints.length > 0;
1034
+ const onMouseUp = reactUseCallbackRef.useCallbackRef((event)=>onRelease(event));
1035
+ if (!modal) {
1036
+ return null;
1037
+ }
1038
+ return /*#__PURE__*/ jsxRuntime.jsx(reactPresence.Presence, {
1039
+ present: isOpen,
1040
+ unmountOnExit: unmountOnExit,
1041
+ lazyMount: lazyMount,
1042
+ children: /*#__PURE__*/ jsxRuntime.jsx(reactPrimitive.Primitive.div, {
1043
+ ref: composedRef,
1044
+ onMouseUp: onMouseUp,
1045
+ "data-snap-points": isOpen && hasSnapPoints ? "true" : "false",
1046
+ "data-snap-points-overlay": isOpen && shouldFade ? "true" : "false",
1047
+ "data-should-overlay-animate": shouldOverlayAnimate ? "true" : "false",
1048
+ "data-open": domUtils.dataAttr(isOpen),
1049
+ "data-animation-done": hasAnimationDone ? "true" : "false",
1050
+ ...props
1051
+ })
1052
+ });
1053
+ });
1054
+ DrawerBackdrop.displayName = "DrawerBackdrop";
1055
+ const DrawerContent = /*#__PURE__*/ React.forwardRef((props, ref)=>{
1056
+ const { style, ...restProps } = props;
1057
+ const { drawerRef, onPress, onRelease, onDrag, keyboardIsOpen, snapPointsOffset, activeSnapPointIndex, modal, isOpen, direction, snapPoints, container, handleOnly, autoFocus, closeDrawer, closeOnInteractOutside, closeOnEscape, dismissible, hasAnimationDone, titleId, descriptionId, lazyMount, unmountOnExit } = useDrawerContext();
1058
+ const [contentNode, setContentNode] = React.useState(null);
1059
+ const contentRef = React.useCallback((el)=>setContentNode(el), []);
1060
+ // aria-hide everything except the content when modal
1061
+ React.useEffect(()=>{
1062
+ if (!isOpen || !modal || !contentNode) return;
1063
+ return ariaHidden.hideOthers(contentNode);
1064
+ }, [
1065
+ isOpen,
1066
+ modal,
1067
+ contentNode
1068
+ ]);
1069
+ // Needed to use transition instead of animations
1070
+ const [delayedSnapPoints, setDelayedSnapPoints] = React.useState(false);
1071
+ const pointerStartRef = React.useRef(null);
1072
+ const lastKnownPointerEventRef = React.useRef(null);
1073
+ const wasBeyondThePointRef = React.useRef(false);
1074
+ const hasSnapPoints = snapPoints && snapPoints.length > 0;
1075
+ const isDeltaInDirection = (delta, dir, threshold = 0)=>{
1076
+ if (wasBeyondThePointRef.current) return true;
1077
+ const deltaY = Math.abs(delta.y);
1078
+ const deltaX = Math.abs(delta.x);
1079
+ const isDeltaX = deltaX > deltaY;
1080
+ const dFactor = [
1081
+ "bottom",
1082
+ "right"
1083
+ ].includes(dir) ? 1 : -1;
1084
+ if (dir === "left" || dir === "right") {
1085
+ const isReverseDirection = delta.x * dFactor < 0;
1086
+ if (!isReverseDirection && deltaX >= 0 && deltaX <= threshold) {
1087
+ return isDeltaX;
1088
+ }
1089
+ } else {
1090
+ const isReverseDirection = delta.y * dFactor < 0;
1091
+ if (!isReverseDirection && deltaY >= 0 && deltaY <= threshold) {
1092
+ return !isDeltaX;
1093
+ }
1094
+ }
1095
+ wasBeyondThePointRef.current = true;
1096
+ return true;
1097
+ };
1098
+ React.useEffect(()=>{
1099
+ if (hasSnapPoints) {
1100
+ window.requestAnimationFrame(()=>{
1101
+ setDelayedSnapPoints(true);
1102
+ });
1103
+ }
1104
+ }, []);
1105
+ function handleOnPointerUp(event) {
1106
+ pointerStartRef.current = null;
1107
+ wasBeyondThePointRef.current = false;
1108
+ onRelease(event);
1109
+ }
1110
+ return /*#__PURE__*/ jsxRuntime.jsx(reactPresence.Presence, {
1111
+ present: isOpen,
1112
+ unmountOnExit: unmountOnExit,
1113
+ lazyMount: lazyMount,
1114
+ children: /*#__PURE__*/ jsxRuntime.jsx(reactDismissibleLayer.DismissibleLayer, {
1115
+ enabled: isOpen,
1116
+ blockPointerEvents: modal,
1117
+ onEscapeKeyDown: (e)=>{
1118
+ if (e.defaultPrevented) return;
1119
+ if (!dismissible || !closeOnEscape) return;
1120
+ closeDrawer(false, {
1121
+ reason: "escapeKeyDown",
1122
+ event: e
1123
+ });
1124
+ },
1125
+ onPressOutside: (e)=>{
1126
+ if (e.defaultPrevented) return;
1127
+ if (!modal) return;
1128
+ if (keyboardIsOpen.current) keyboardIsOpen.current = false;
1129
+ if (!dismissible || !closeOnInteractOutside) return;
1130
+ closeDrawer(false, {
1131
+ reason: "interactOutside",
1132
+ event: e
1133
+ });
1134
+ },
1135
+ onFocusOutside: ()=>{
1136
+ // Focus trapping is handled by FocusScope — nothing to do here.
1137
+ // The old Radix architecture needed e.preventDefault() here (PR #1187)
1138
+ // to prevent cascade closes, but the new DismissibleLayer handles
1139
+ // that via onCascadeDismiss instead.
1140
+ },
1141
+ onCascadeDismiss: ({ dismissedParent })=>{
1142
+ closeDrawer(false, {
1143
+ reason: "cascadeDismiss",
1144
+ dismissedParent
1145
+ });
1146
+ },
1147
+ children: /*#__PURE__*/ jsxRuntime.jsx(reactFocusScope.FocusScope, {
1148
+ asChild: true,
1149
+ loop: modal,
1150
+ trapped: modal && isOpen,
1151
+ onMountAutoFocus: (e)=>{
1152
+ // prevent FocusScope's default autoFocus behavior
1153
+ e.preventDefault();
1154
+ // when autoFocus is true, FocusScope sets the focus to the first tabbable element; otherwise content;
1155
+ // the desired behavior is to set the focus to the content regardless of whether there are tabbable elements or not when true]
1156
+ if (autoFocus) {
1157
+ drawerRef.current?.focus();
1158
+ }
1159
+ // later, we can do something like:
1160
+ // when trigger has clicked using keyboard, focus the first tabbable element;
1161
+ // when trigger has clicked using mouse, focus the content
1162
+ // -> matches Menu behavior
1163
+ },
1164
+ children: /*#__PURE__*/ jsxRuntime.jsx(reactPrimitive.Primitive.div, {
1165
+ role: "dialog",
1166
+ "aria-modal": modal,
1167
+ "aria-labelledby": titleId,
1168
+ "aria-describedby": descriptionId,
1169
+ "data-delayed-snap-points": delayedSnapPoints ? "true" : "false",
1170
+ "data-drawer-direction": direction,
1171
+ "data-open": domUtils.dataAttr(isOpen),
1172
+ "data-animation-done": hasAnimationDone ? "true" : "false",
1173
+ "data-drawer": "",
1174
+ "data-snap-points": isOpen && hasSnapPoints ? "true" : "false",
1175
+ "data-custom-container": container ? "true" : "false",
1176
+ ...restProps,
1177
+ ref: reactComposeRefs.composeRefs(ref, drawerRef, contentRef),
1178
+ style: {
1179
+ ...snapPointsOffset && snapPointsOffset.length > 0 ? {
1180
+ "--snap-point-height": `${snapPointsOffset[activeSnapPointIndex ?? 0]}px`,
1181
+ ...style
1182
+ } : style,
1183
+ ...!modal && {
1184
+ pointerEvents: "auto"
1185
+ }
1186
+ },
1187
+ onPointerDown: (event)=>{
1188
+ if (handleOnly) return;
1189
+ restProps.onPointerDown?.(event);
1190
+ pointerStartRef.current = {
1191
+ x: event.pageX,
1192
+ y: event.pageY
1193
+ };
1194
+ onPress(event);
1195
+ },
1196
+ onPointerMove: (event)=>{
1197
+ lastKnownPointerEventRef.current = event;
1198
+ if (handleOnly) return;
1199
+ restProps.onPointerMove?.(event);
1200
+ if (!pointerStartRef.current) return;
1201
+ const yPosition = event.pageY - pointerStartRef.current.y;
1202
+ const xPosition = event.pageX - pointerStartRef.current.x;
1203
+ const swipeStartThreshold = event.pointerType === "touch" ? 10 : 2;
1204
+ const delta = {
1205
+ x: xPosition,
1206
+ y: yPosition
1207
+ };
1208
+ const isAllowedToSwipe = isDeltaInDirection(delta, direction, swipeStartThreshold);
1209
+ if (isAllowedToSwipe) onDrag(event);
1210
+ else if (Math.abs(xPosition) > swipeStartThreshold || Math.abs(yPosition) > swipeStartThreshold) {
1211
+ pointerStartRef.current = null;
1212
+ }
1213
+ },
1214
+ onPointerUp: (event)=>{
1215
+ restProps.onPointerUp?.(event);
1216
+ pointerStartRef.current = null;
1217
+ wasBeyondThePointRef.current = false;
1218
+ onRelease(event);
1219
+ },
1220
+ onPointerOut: (event)=>{
1221
+ restProps.onPointerOut?.(event);
1222
+ handleOnPointerUp(lastKnownPointerEventRef.current);
1223
+ },
1224
+ onContextMenu: (event)=>{
1225
+ restProps.onContextMenu?.(event);
1226
+ if (lastKnownPointerEventRef.current) {
1227
+ handleOnPointerUp(lastKnownPointerEventRef.current);
1228
+ }
1229
+ }
1230
+ })
1231
+ })
1232
+ })
1233
+ });
1234
+ });
1235
+ DrawerContent.displayName = "DrawerContent";
1236
+ const DrawerTitle = /*#__PURE__*/ React.forwardRef((props, ref)=>{
1237
+ const api = useDrawerContext();
1238
+ return /*#__PURE__*/ jsxRuntime.jsx(reactPrimitive.Primitive.h2, {
1239
+ ref: ref,
1240
+ ...domUtils.mergeProps(api.titleProps, props)
1241
+ });
1242
+ });
1243
+ DrawerTitle.displayName = "DrawerTitle";
1244
+ const DrawerDescription = /*#__PURE__*/ React.forwardRef((props, ref)=>{
1245
+ const api = useDrawerContext();
1246
+ return /*#__PURE__*/ jsxRuntime.jsx(reactPrimitive.Primitive.p, {
1247
+ ref: ref,
1248
+ ...domUtils.mergeProps(api.descriptionProps, props)
1249
+ });
1250
+ });
1251
+ DrawerDescription.displayName = "DrawerDescription";
1252
+ const DrawerHeader = /*#__PURE__*/ React.forwardRef((props, ref)=>{
1253
+ const api = useDrawerContext();
1254
+ return /*#__PURE__*/ jsxRuntime.jsx(reactPrimitive.Primitive.div, {
1255
+ ref: ref,
1256
+ ...domUtils.mergeProps(api.headerProps, props)
1257
+ });
1258
+ });
1259
+ DrawerHeader.displayName = "DrawerHeader";
1260
+ const DrawerCloseButton = /*#__PURE__*/ React.forwardRef((props, ref)=>{
1261
+ const api = useDrawerContext();
1262
+ return /*#__PURE__*/ jsxRuntime.jsx(reactPrimitive.Primitive.button, {
1263
+ ref: reactComposeRefs.composeRefs(ref, api.closeButtonRef),
1264
+ ...domUtils.mergeProps(api.closeButtonProps, props)
1265
+ });
1266
+ });
1267
+ const LONG_HANDLE_PRESS_TIMEOUT = 250;
1268
+ const DOUBLE_TAP_TIMEOUT = 120;
1269
+ const DrawerHandle = /*#__PURE__*/ React.forwardRef((props, ref)=>{
1270
+ const { preventCycle = false, children, ...rest } = props;
1271
+ const { closeDrawer, isDragging, snapPoints, activeSnapPoint, setActiveSnapPoint, dismissible, handleOnly, isOpen, onPress, onDrag, onRelease } = useDrawerContext();
1272
+ const closeTimeoutIdRef = React.useRef(null);
1273
+ const shouldCancelInteractionRef = React.useRef(false);
1274
+ function handleStartCycle(event) {
1275
+ // Stop if this is the second click of a double click
1276
+ if (shouldCancelInteractionRef.current) {
1277
+ handleCancelInteraction();
1278
+ return;
1279
+ }
1280
+ window.setTimeout(()=>{
1281
+ handleCycleSnapPoints(event);
1282
+ }, DOUBLE_TAP_TIMEOUT);
1283
+ }
1284
+ function handleCycleSnapPoints(event) {
1285
+ // Prevent accidental taps while resizing drawer
1286
+ if (isDragging || preventCycle || shouldCancelInteractionRef.current) {
1287
+ handleCancelInteraction();
1288
+ return;
1289
+ }
1290
+ // Make sure to clear the timeout id if the user releases the handle before the cancel timeout
1291
+ handleCancelInteraction();
1292
+ if (!snapPoints || snapPoints.length === 0) {
1293
+ if (!dismissible) {
1294
+ closeDrawer(false, {
1295
+ reason: "handleClickOnLastSnapPoint",
1296
+ event: event.nativeEvent
1297
+ });
1298
+ }
1299
+ return;
1300
+ }
1301
+ const isLastSnapPoint = activeSnapPoint === snapPoints[snapPoints.length - 1];
1302
+ if (isLastSnapPoint && dismissible) {
1303
+ closeDrawer(false, {
1304
+ reason: "handleClickOnLastSnapPoint",
1305
+ event: event.nativeEvent
1306
+ });
1307
+ return;
1308
+ }
1309
+ const currentSnapIndex = snapPoints.findIndex((point)=>point === activeSnapPoint);
1310
+ if (currentSnapIndex === -1) return; // activeSnapPoint not found in snapPoints
1311
+ const nextSnapPoint = snapPoints[currentSnapIndex + 1];
1312
+ setActiveSnapPoint(nextSnapPoint);
1313
+ }
1314
+ function handleStartInteraction() {
1315
+ closeTimeoutIdRef.current = window.setTimeout(()=>{
1316
+ // Cancel click interaction on a long press
1317
+ shouldCancelInteractionRef.current = true;
1318
+ }, LONG_HANDLE_PRESS_TIMEOUT);
1319
+ }
1320
+ function handleCancelInteraction() {
1321
+ if (closeTimeoutIdRef.current) {
1322
+ window.clearTimeout(closeTimeoutIdRef.current);
1323
+ }
1324
+ shouldCancelInteractionRef.current = false;
1325
+ }
1326
+ return /*#__PURE__*/ jsxRuntime.jsx(reactPrimitive.Primitive.div, {
1327
+ ref: ref,
1328
+ onClick: handleStartCycle,
1329
+ onPointerCancel: handleCancelInteraction,
1330
+ onPointerDown: (e)=>{
1331
+ if (handleOnly) onPress(e);
1332
+ handleStartInteraction();
1333
+ },
1334
+ onPointerMove: (e)=>{
1335
+ if (handleOnly) onDrag(e);
1336
+ },
1337
+ onPointerUp: (e)=>{
1338
+ if (handleOnly) onRelease(e);
1339
+ handleCancelInteraction();
1340
+ },
1341
+ "data-drawer-visible": isOpen ? "true" : "false",
1342
+ "data-handle": "",
1343
+ "aria-hidden": "true",
1344
+ ...rest,
1345
+ children: children
1346
+ });
1347
+ });
1348
+ DrawerHandle.displayName = "DrawerHandle";
1349
+
1350
+ exports.DrawerBackdrop = DrawerBackdrop;
1351
+ exports.DrawerCloseButton = DrawerCloseButton;
1352
+ exports.DrawerContent = DrawerContent;
1353
+ exports.DrawerDescription = DrawerDescription;
1354
+ exports.DrawerHandle = DrawerHandle;
1355
+ exports.DrawerHeader = DrawerHeader;
1356
+ exports.DrawerPositioner = DrawerPositioner;
1357
+ exports.DrawerRoot = DrawerRoot;
1358
+ exports.DrawerTitle = DrawerTitle;
1359
+ exports.DrawerTrigger = DrawerTrigger;
1360
+ exports.useDrawer = useDrawer;
1361
+ exports.useDrawerContext = useDrawerContext;