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