@lodev09/react-native-true-sheet 3.10.1 → 3.11.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/lib/module/TrueSheet.js +3 -0
  2. package/lib/module/TrueSheet.js.map +1 -1
  3. package/lib/module/TrueSheet.web.js +604 -376
  4. package/lib/module/TrueSheet.web.js.map +1 -1
  5. package/lib/module/TrueSheetProvider.js.map +1 -1
  6. package/lib/module/TrueSheetProvider.web.js +141 -100
  7. package/lib/module/TrueSheetProvider.web.js.map +1 -1
  8. package/lib/module/mocks/index.js.map +1 -1
  9. package/lib/module/reanimated/ReanimatedTrueSheet.web.js.map +1 -1
  10. package/lib/module/reanimated/useReanimatedPositionChangeHandler.web.js +2 -3
  11. package/lib/module/reanimated/useReanimatedPositionChangeHandler.web.js.map +1 -1
  12. package/lib/module/web/constants.js +16 -0
  13. package/lib/module/web/constants.js.map +1 -0
  14. package/lib/module/web/vaul/README.md +9 -0
  15. package/lib/module/web/vaul/browser.js +30 -0
  16. package/lib/module/web/vaul/browser.js.map +1 -0
  17. package/lib/module/web/vaul/constants.js +14 -0
  18. package/lib/module/web/vaul/constants.js.map +1 -0
  19. package/lib/module/web/vaul/context.js +57 -0
  20. package/lib/module/web/vaul/context.js.map +1 -0
  21. package/lib/module/web/vaul/helpers.js +93 -0
  22. package/lib/module/web/vaul/helpers.js.map +1 -0
  23. package/lib/module/web/vaul/index.js +1206 -0
  24. package/lib/module/web/vaul/index.js.map +1 -0
  25. package/lib/module/web/vaul/style.css +264 -0
  26. package/lib/module/web/vaul/types.js +2 -0
  27. package/lib/module/web/vaul/types.js.map +1 -0
  28. package/lib/module/web/vaul/use-composed-refs.js +34 -0
  29. package/lib/module/web/vaul/use-composed-refs.js.map +1 -0
  30. package/lib/module/web/vaul/use-controllable-state.js +54 -0
  31. package/lib/module/web/vaul/use-controllable-state.js.map +1 -0
  32. package/lib/module/web/vaul/use-position-fixed.js +123 -0
  33. package/lib/module/web/vaul/use-position-fixed.js.map +1 -0
  34. package/lib/module/web/vaul/use-prevent-scroll.js +258 -0
  35. package/lib/module/web/vaul/use-prevent-scroll.js.map +1 -0
  36. package/lib/module/web/vaul/use-scale-background.js +57 -0
  37. package/lib/module/web/vaul/use-scale-background.js.map +1 -0
  38. package/lib/module/web/vaul/use-snap-points.js +294 -0
  39. package/lib/module/web/vaul/use-snap-points.js.map +1 -0
  40. package/lib/typescript/src/TrueSheet.d.ts +2 -2
  41. package/lib/typescript/src/TrueSheet.d.ts.map +1 -1
  42. package/lib/typescript/src/TrueSheet.types.d.ts +79 -36
  43. package/lib/typescript/src/TrueSheet.types.d.ts.map +1 -1
  44. package/lib/typescript/src/TrueSheet.web.d.ts +3 -11
  45. package/lib/typescript/src/TrueSheet.web.d.ts.map +1 -1
  46. package/lib/typescript/src/TrueSheetProvider.d.ts +2 -2
  47. package/lib/typescript/src/TrueSheetProvider.d.ts.map +1 -1
  48. package/lib/typescript/src/TrueSheetProvider.web.d.ts +17 -14
  49. package/lib/typescript/src/TrueSheetProvider.web.d.ts.map +1 -1
  50. package/lib/typescript/src/mocks/index.d.ts +1 -2
  51. package/lib/typescript/src/mocks/index.d.ts.map +1 -1
  52. package/lib/typescript/src/navigation/types.d.ts +1 -1
  53. package/lib/typescript/src/navigation/types.d.ts.map +1 -1
  54. package/lib/typescript/src/reanimated/ReanimatedTrueSheet.web.d.ts +0 -1
  55. package/lib/typescript/src/reanimated/ReanimatedTrueSheet.web.d.ts.map +1 -1
  56. package/lib/typescript/src/reanimated/useReanimatedPositionChangeHandler.web.d.ts +2 -3
  57. package/lib/typescript/src/reanimated/useReanimatedPositionChangeHandler.web.d.ts.map +1 -1
  58. package/lib/typescript/src/web/constants.d.ts +14 -0
  59. package/lib/typescript/src/web/constants.d.ts.map +1 -0
  60. package/lib/typescript/src/web/vaul/browser.d.ts +8 -0
  61. package/lib/typescript/src/web/vaul/browser.d.ts.map +1 -0
  62. package/lib/typescript/src/web/vaul/constants.d.ts +12 -0
  63. package/lib/typescript/src/web/vaul/constants.d.ts.map +1 -0
  64. package/lib/typescript/src/web/vaul/context.d.ts +45 -0
  65. package/lib/typescript/src/web/vaul/context.d.ts.map +1 -0
  66. package/lib/typescript/src/web/vaul/helpers.d.ts +17 -0
  67. package/lib/typescript/src/web/vaul/helpers.d.ts.map +1 -0
  68. package/lib/typescript/src/web/vaul/index.d.ts +210 -0
  69. package/lib/typescript/src/web/vaul/index.d.ts.map +1 -0
  70. package/lib/typescript/src/web/vaul/types.d.ts +7 -0
  71. package/lib/typescript/src/web/vaul/types.d.ts.map +1 -0
  72. package/lib/typescript/src/web/vaul/use-composed-refs.d.ts +14 -0
  73. package/lib/typescript/src/web/vaul/use-composed-refs.d.ts.map +1 -0
  74. package/lib/typescript/src/web/vaul/use-controllable-state.d.ts +9 -0
  75. package/lib/typescript/src/web/vaul/use-controllable-state.d.ts.map +1 -0
  76. package/lib/typescript/src/web/vaul/use-position-fixed.d.ts +19 -0
  77. package/lib/typescript/src/web/vaul/use-position-fixed.d.ts.map +1 -0
  78. package/lib/typescript/src/web/vaul/use-prevent-scroll.d.ts +18 -0
  79. package/lib/typescript/src/web/vaul/use-prevent-scroll.d.ts.map +1 -0
  80. package/lib/typescript/src/web/vaul/use-scale-background.d.ts +2 -0
  81. package/lib/typescript/src/web/vaul/use-scale-background.d.ts.map +1 -0
  82. package/lib/typescript/src/web/vaul/use-snap-points.d.ts +37 -0
  83. package/lib/typescript/src/web/vaul/use-snap-points.d.ts.map +1 -0
  84. package/package.json +4 -4
  85. package/src/TrueSheet.tsx +9 -1
  86. package/src/TrueSheet.types.ts +88 -38
  87. package/src/TrueSheet.web.tsx +675 -450
  88. package/src/TrueSheetProvider.tsx +2 -4
  89. package/src/TrueSheetProvider.web.tsx +193 -125
  90. package/src/mocks/index.ts +1 -2
  91. package/src/navigation/types.ts +0 -1
  92. package/src/reanimated/ReanimatedTrueSheet.web.tsx +0 -1
  93. package/src/reanimated/useReanimatedPositionChangeHandler.web.ts +2 -3
  94. package/src/web/constants.ts +13 -0
  95. package/src/web/vaul/README.md +9 -0
  96. package/src/web/vaul/browser.ts +38 -0
  97. package/src/web/vaul/constants.ts +18 -0
  98. package/src/web/vaul/context.ts +87 -0
  99. package/src/web/vaul/helpers.ts +124 -0
  100. package/src/web/vaul/index.tsx +1590 -0
  101. package/src/web/vaul/style.css +264 -0
  102. package/src/web/vaul/types.ts +7 -0
  103. package/src/web/vaul/use-composed-refs.ts +35 -0
  104. package/src/web/vaul/use-controllable-state.ts +66 -0
  105. package/src/web/vaul/use-position-fixed.ts +147 -0
  106. package/src/web/vaul/use-prevent-scroll.ts +309 -0
  107. package/src/web/vaul/use-scale-background.ts +64 -0
  108. package/src/web/vaul/use-snap-points.ts +433 -0
@@ -0,0 +1,1206 @@
1
+ "use strict";
2
+ 'use client';
3
+
4
+ import * as DialogPrimitive from '@radix-ui/react-dialog';
5
+ import React from 'react';
6
+ import { DrawerContext, useDrawerContext } from "./context.js";
7
+ import './style.css';
8
+ import { usePreventScroll, isInput } from "./use-prevent-scroll.js";
9
+ import { useComposedRefs } from "./use-composed-refs.js";
10
+ import { useSnapPoints } from "./use-snap-points.js";
11
+ import { set, getTranslate, dampenValue, isVertical, reset } from "./helpers.js";
12
+ import { TRANSITIONS, VELOCITY_THRESHOLD, CLOSE_THRESHOLD, SCROLL_LOCK_TIMEOUT, BORDER_RADIUS, NESTED_DISPLACEMENT, WINDOW_TOP_OFFSET, DRAG_CLASS } from "./constants.js";
13
+ import { useControllableState } from "./use-controllable-state.js";
14
+ import { useScaleBackground } from "./use-scale-background.js";
15
+ import { usePositionFixed } from "./use-position-fixed.js";
16
+ import { isIOS, isMobileFirefox } from "./browser.js";
17
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
18
+ export function Root({
19
+ open: openProp,
20
+ onOpenChange,
21
+ children,
22
+ onDrag: onDragProp,
23
+ onRelease: onReleaseProp,
24
+ snapPoints,
25
+ shouldScaleBackground = false,
26
+ setBackgroundColorOnScale = true,
27
+ closeThreshold = CLOSE_THRESHOLD,
28
+ scrollLockTimeout = SCROLL_LOCK_TIMEOUT,
29
+ dismissible = true,
30
+ draggable = true,
31
+ handleOnly = false,
32
+ fadeFromIndex = snapPoints && snapPoints.length - 1,
33
+ activeSnapPoint: activeSnapPointProp,
34
+ setActiveSnapPoint: setActiveSnapPointProp,
35
+ fixed,
36
+ modal = true,
37
+ onClose,
38
+ nested,
39
+ noBodyStyles = false,
40
+ direction = 'bottom',
41
+ defaultOpen = false,
42
+ disablePreventScroll = true,
43
+ snapToSequentialPoint = false,
44
+ preventScrollRestoration = false,
45
+ repositionInputs = true,
46
+ onAnimationEnd,
47
+ onPositionChange,
48
+ container,
49
+ autoFocus = false,
50
+ detached = false,
51
+ detachedOffset = 0,
52
+ detachedRadius = 0,
53
+ detachedWrapperStyle,
54
+ maxContentHeight,
55
+ initialAnimated = true
56
+ }) {
57
+ const [isOpen = false, setIsOpen] = useControllableState({
58
+ defaultProp: defaultOpen,
59
+ prop: openProp,
60
+ onChange: o => {
61
+ onOpenChange?.(o);
62
+ if (!o && !nested) {
63
+ restorePositionSetting();
64
+ }
65
+ setTimeout(() => {
66
+ onAnimationEnd?.(o);
67
+ }, TRANSITIONS.DURATION * 1000);
68
+ if (o && !modal) {
69
+ if (typeof window !== 'undefined') {
70
+ window.requestAnimationFrame(() => {
71
+ document.body.style.pointerEvents = 'auto';
72
+ });
73
+ }
74
+ }
75
+ if (!o) {
76
+ // This will be removed when the exit animation ends (`500ms`)
77
+ document.body.style.pointerEvents = 'auto';
78
+ }
79
+ }
80
+ });
81
+ const [hasBeenOpened, setHasBeenOpened] = React.useState(false);
82
+ const [isDragging, setIsDragging] = React.useState(false);
83
+ const [justReleased, setJustReleased] = React.useState(false);
84
+ const overlayRef = React.useRef(null);
85
+ const openTime = React.useRef(null);
86
+ const dragStartTime = React.useRef(null);
87
+ const dragEndTime = React.useRef(null);
88
+ const lastTimeDragPrevented = React.useRef(null);
89
+ const isAllowedToDrag = React.useRef(false);
90
+ const nestedOpenChangeTimer = React.useRef(null);
91
+ const pointerStart = React.useRef(0);
92
+ const keyboardIsOpen = React.useRef(false);
93
+ const shouldAnimate = React.useRef(!defaultOpen);
94
+ const previousDiffFromInitial = React.useRef(0);
95
+ const drawerRef = React.useRef(null);
96
+ const drawerHeightRef = React.useRef(drawerRef.current?.getBoundingClientRect().height || 0);
97
+ const drawerWidthRef = React.useRef(drawerRef.current?.getBoundingClientRect().width || 0);
98
+ const initialDrawerHeight = React.useRef(0);
99
+ const [contentHeight, setContentHeight] = React.useState(0);
100
+ const onSnapPointChange = React.useCallback(activeSnapPointIndex => {
101
+ // Change openTime ref when we reach the last snap point to prevent dragging for 500ms incase it's scrollable.
102
+ if (snapPoints && activeSnapPointIndex === snapPointsOffset.length - 1) openTime.current = new Date();
103
+ }, []);
104
+ const {
105
+ activeSnapPoint,
106
+ activeSnapPointIndex,
107
+ setActiveSnapPoint,
108
+ onRelease: onReleaseSnapPoints,
109
+ snapPointsOffset,
110
+ onDrag: onDragSnapPoints,
111
+ shouldFade,
112
+ getPercentageDragged: getSnapPointsPercentageDragged
113
+ } = useSnapPoints({
114
+ snapPoints,
115
+ activeSnapPointProp,
116
+ setActiveSnapPointProp,
117
+ drawerRef,
118
+ fadeFromIndex,
119
+ overlayRef,
120
+ onSnapPointChange,
121
+ direction,
122
+ container,
123
+ snapToSequentialPoint,
124
+ isOpen,
125
+ contentHeight,
126
+ detachedOffset: detached ? detachedOffset : 0,
127
+ maxContentHeight,
128
+ initialAnimated
129
+ });
130
+ usePreventScroll({
131
+ isDisabled: !isOpen || isDragging || !modal || justReleased || !hasBeenOpened || !repositionInputs || !disablePreventScroll
132
+ });
133
+ const {
134
+ restorePositionSetting
135
+ } = usePositionFixed({
136
+ isOpen,
137
+ modal,
138
+ nested: nested ?? false,
139
+ hasBeenOpened,
140
+ preventScrollRestoration,
141
+ noBodyStyles
142
+ });
143
+ function getScale() {
144
+ return (window.innerWidth - WINDOW_TOP_OFFSET) / window.innerWidth;
145
+ }
146
+ function onPress(event) {
147
+ if (!draggable) return;
148
+ if (!dismissible && !snapPoints) return;
149
+ if (drawerRef.current && !drawerRef.current.contains(event.target)) return;
150
+ drawerHeightRef.current = drawerRef.current?.getBoundingClientRect().height || 0;
151
+ drawerWidthRef.current = drawerRef.current?.getBoundingClientRect().width || 0;
152
+ setIsDragging(true);
153
+ dragStartTime.current = new Date();
154
+
155
+ // iOS doesn't trigger mouseUp after scrolling so we need to listen to touched in order to disallow dragging
156
+ if (isIOS()) {
157
+ window.addEventListener('touchend', () => isAllowedToDrag.current = false, {
158
+ once: true
159
+ });
160
+ }
161
+ // Ensure we maintain correct pointer capture even when going outside of the drawer
162
+ event.target.setPointerCapture(event.pointerId);
163
+ pointerStart.current = isVertical(direction) ? event.pageY : event.pageX;
164
+ }
165
+ function shouldDrag(el, isDraggingInDirection) {
166
+ let element = el;
167
+ const highlightedText = window.getSelection()?.toString();
168
+ const swipeAmount = drawerRef.current ? getTranslate(drawerRef.current, direction) : null;
169
+ const date = new Date();
170
+
171
+ // Fixes https://github.com/emilkowalski/vaul/issues/483
172
+ if (element.tagName === 'SELECT') {
173
+ return false;
174
+ }
175
+ if (element.hasAttribute('data-vaul-no-drag') || element.closest('[data-vaul-no-drag]')) {
176
+ return false;
177
+ }
178
+ if (direction === 'right' || direction === 'left') {
179
+ return true;
180
+ }
181
+
182
+ // Allow scrolling when animating
183
+ if (openTime.current && date.getTime() - openTime.current.getTime() < 500) {
184
+ return false;
185
+ }
186
+ if (swipeAmount !== null) {
187
+ if (direction === 'bottom' ? swipeAmount > 0 : swipeAmount < 0) {
188
+ return true;
189
+ }
190
+ }
191
+
192
+ // Don't drag if there's highlighted text
193
+ if (highlightedText && highlightedText.length > 0) {
194
+ return false;
195
+ }
196
+
197
+ // Disallow dragging if drawer was scrolled within `scrollLockTimeout`
198
+ if (lastTimeDragPrevented.current && date.getTime() - lastTimeDragPrevented.current.getTime() < scrollLockTimeout && swipeAmount === 0) {
199
+ lastTimeDragPrevented.current = date;
200
+ return false;
201
+ }
202
+ if (isDraggingInDirection) {
203
+ lastTimeDragPrevented.current = date;
204
+
205
+ // We are dragging down so we should allow scrolling
206
+ return false;
207
+ }
208
+
209
+ // Keep climbing up the DOM tree as long as there's a parent
210
+ while (element) {
211
+ // Check if the element is scrollable
212
+ if (element.scrollHeight > element.clientHeight) {
213
+ if (element.scrollTop !== 0) {
214
+ lastTimeDragPrevented.current = new Date();
215
+
216
+ // The element is scrollable and not scrolled to the top, so don't drag
217
+ return false;
218
+ }
219
+ if (element.getAttribute('role') === 'dialog') {
220
+ return true;
221
+ }
222
+ }
223
+
224
+ // Move up to the parent element
225
+ element = element.parentNode;
226
+ }
227
+
228
+ // No scrollable parents not scrolled to the top found, so drag
229
+ return true;
230
+ }
231
+ function onDrag(event) {
232
+ if (!drawerRef.current) {
233
+ return;
234
+ }
235
+
236
+ // We need to know how much of the drawer has been dragged in percentages so that we can transform background accordingly
237
+ if (isDragging) {
238
+ const directionMultiplier = direction === 'bottom' || direction === 'right' ? 1 : -1;
239
+ const draggedDistance = (pointerStart.current - (isVertical(direction) ? event.pageY : event.pageX)) * directionMultiplier;
240
+ const isDraggingInDirection = draggedDistance > 0;
241
+
242
+ // Pre condition for disallowing dragging in the close direction.
243
+ const noCloseSnapPointsPreCondition = snapPoints && !dismissible && !isDraggingInDirection;
244
+
245
+ // Disallow dragging down to close when first snap point is the active one and dismissible prop is set to false.
246
+ if (noCloseSnapPointsPreCondition && activeSnapPointIndex === 0) return;
247
+
248
+ // We need to capture last time when drag with scroll was triggered and have a timeout between
249
+ const absDraggedDistance = Math.abs(draggedDistance);
250
+ const wrapper = document.querySelector('[data-vaul-drawer-wrapper]');
251
+ const drawerDimension = direction === 'bottom' || direction === 'top' ? drawerHeightRef.current : drawerWidthRef.current;
252
+
253
+ // Calculate the percentage dragged, where 1 is the closed position
254
+ let percentageDragged = absDraggedDistance / drawerDimension;
255
+ const snapPointPercentageDragged = getSnapPointsPercentageDragged(absDraggedDistance, isDraggingInDirection);
256
+ if (snapPointPercentageDragged !== null) {
257
+ percentageDragged = snapPointPercentageDragged;
258
+ }
259
+
260
+ // Disallow close dragging beyond the smallest snap point.
261
+ if (noCloseSnapPointsPreCondition && percentageDragged >= 1) {
262
+ return;
263
+ }
264
+ if (!isAllowedToDrag.current && !shouldDrag(event.target, isDraggingInDirection)) return;
265
+ drawerRef.current.classList.add(DRAG_CLASS);
266
+ // If shouldDrag gave true once after pressing down on the drawer, we set isAllowedToDrag to true and it will remain true until we let go, there's no reason to disable dragging mid way, ever, and that's the solution to it
267
+ isAllowedToDrag.current = true;
268
+ set(drawerRef.current, {
269
+ transition: 'none'
270
+ });
271
+ set(overlayRef.current, {
272
+ transition: 'none'
273
+ });
274
+ onDragProp?.(event, percentageDragged);
275
+ if (snapPoints) {
276
+ onDragSnapPoints({
277
+ draggedDistance
278
+ });
279
+ }
280
+
281
+ // Run this only if snapPoints are not defined or if we are at the last snap point (highest one)
282
+ if (isDraggingInDirection && !snapPoints) {
283
+ const dampenedDraggedDistance = dampenValue(draggedDistance);
284
+ const translateValue = Math.min(dampenedDraggedDistance * -1, 0) * directionMultiplier;
285
+ set(drawerRef.current, {
286
+ transform: isVertical(direction) ? `translate3d(0, ${translateValue}px, 0)` : `translate3d(${translateValue}px, 0, 0)`
287
+ });
288
+ return;
289
+ }
290
+ const opacityValue = 1 - percentageDragged;
291
+ if (shouldFade || fadeFromIndex && activeSnapPointIndex === fadeFromIndex - 1) {
292
+ set(overlayRef.current, {
293
+ opacity: `${opacityValue}`,
294
+ transition: 'none'
295
+ }, true);
296
+ }
297
+ if (wrapper && overlayRef.current && shouldScaleBackground) {
298
+ // Calculate percentageDragged as a fraction (0 to 1)
299
+ const scaleValue = Math.min(getScale() + percentageDragged * (1 - getScale()), 1);
300
+ const borderRadiusValue = 8 - percentageDragged * 8;
301
+ const translateValue = Math.max(0, 14 - percentageDragged * 14);
302
+ set(wrapper, {
303
+ borderRadius: `${borderRadiusValue}px`,
304
+ transform: isVertical(direction) ? `scale(${scaleValue}) translate3d(0, ${translateValue}px, 0)` : `scale(${scaleValue}) translate3d(${translateValue}px, 0, 0)`,
305
+ transition: 'none'
306
+ }, true);
307
+ }
308
+ if (!snapPoints) {
309
+ const translateValue = absDraggedDistance * directionMultiplier;
310
+ set(drawerRef.current, {
311
+ transform: isVertical(direction) ? `translate3d(0, ${translateValue}px, 0)` : `translate3d(${translateValue}px, 0, 0)`
312
+ });
313
+ }
314
+ }
315
+ }
316
+ React.useEffect(() => {
317
+ window.requestAnimationFrame(() => {
318
+ shouldAnimate.current = true;
319
+ });
320
+ }, []);
321
+
322
+ // Reset to the first snap point on every open so a prior session's active snap
323
+ // (kept in state by `useControllableState` after dismissal) doesn't leak into
324
+ // the next present. Skipped when `activeSnapPoint` is controlled externally.
325
+ React.useEffect(() => {
326
+ if (isOpen && snapPoints && activeSnapPointProp === undefined) {
327
+ setActiveSnapPoint(snapPoints[0]);
328
+ }
329
+ }, [isOpen]);
330
+ const onPositionChangeRef = React.useRef(onPositionChange);
331
+ React.useEffect(() => {
332
+ onPositionChangeRef.current = onPositionChange;
333
+ });
334
+
335
+ // Radix's DismissableLayer (modal) sets body pointer-events to "none" so the
336
+ // overlay traps clicks. When the active snap is below fadeFromIndex the
337
+ // overlay is fully transparent, and we want clicks to reach the content
338
+ // behind the drawer — so we restore body interactivity. DismissableLayer
339
+ // captures its DOM node via a ref-callback `setNode`, so its body write
340
+ // runs one render later than ours on initial open — re-assert on rAF so
341
+ // the final write is ours.
342
+ React.useEffect(() => {
343
+ if (!modal || !snapPoints || fadeFromIndex === undefined) return;
344
+ // On close, restore body interactivity. `useControllableState.onChange`
345
+ // only fires for internal `setIsOpen` calls — when the parent flips the
346
+ // `open` prop imperatively, the reset at line ~201 never runs.
347
+ if (!isOpen) {
348
+ document.body.style.pointerEvents = 'auto';
349
+ return;
350
+ }
351
+ if (typeof activeSnapPointIndex !== 'number') return;
352
+ const isBelowFade = activeSnapPointIndex < fadeFromIndex;
353
+ const value = isBelowFade ? 'auto' : 'none';
354
+ document.body.style.pointerEvents = value;
355
+ const id = window.requestAnimationFrame(() => {
356
+ document.body.style.pointerEvents = value;
357
+ });
358
+
359
+ // React Navigation keeps unfocused screens mounted with `display: none` on
360
+ // web, so the drawer never unmounts and neither `isOpen` nor this effect's
361
+ // deps change — a lingering `none` body style would then leak to whichever
362
+ // screen becomes visible. Observe the drawer: when an ancestor hides it,
363
+ // restore `auto`; when it returns, re-apply the desired value.
364
+ let observer = null;
365
+ let rafId = 0;
366
+ const startObserving = () => {
367
+ const node = drawerRef.current;
368
+ if (!node) {
369
+ rafId = window.requestAnimationFrame(startObserving);
370
+ return;
371
+ }
372
+ observer = new IntersectionObserver(entries => {
373
+ const entry = entries[entries.length - 1];
374
+ if (!entry) return;
375
+ document.body.style.pointerEvents = entry.isIntersecting ? value : 'auto';
376
+ });
377
+ observer.observe(node);
378
+ };
379
+ rafId = window.requestAnimationFrame(startObserving);
380
+
381
+ // Always restore on cleanup so an unmount while the sheet is still open
382
+ // doesn't leave the page uninteractive. If the effect re-runs for a
383
+ // normal state change, it re-applies the correct value.
384
+ return () => {
385
+ window.cancelAnimationFrame(id);
386
+ window.cancelAnimationFrame(rafId);
387
+ observer?.disconnect();
388
+ document.body.style.pointerEvents = 'auto';
389
+ };
390
+ }, [isOpen, modal, snapPoints, fadeFromIndex, activeSnapPointIndex]);
391
+ React.useEffect(() => {
392
+ function onVisualViewportChange() {
393
+ if (!drawerRef.current || !repositionInputs) return;
394
+ const focusedElement = document.activeElement;
395
+ if (isInput(focusedElement) || keyboardIsOpen.current) {
396
+ const visualViewportHeight = window.visualViewport?.height || 0;
397
+ const totalHeight = window.innerHeight;
398
+ // This is the height of the keyboard
399
+ let diffFromInitial = totalHeight - visualViewportHeight;
400
+ const drawerHeight = drawerRef.current.getBoundingClientRect().height || 0;
401
+ // Adjust drawer height only if it's tall enough
402
+ const isTallEnough = drawerHeight > totalHeight * 0.8;
403
+ if (!initialDrawerHeight.current) {
404
+ initialDrawerHeight.current = drawerHeight;
405
+ }
406
+ const offsetFromTop = drawerRef.current.getBoundingClientRect().top;
407
+
408
+ // visualViewport height may change due to somq e subtle changes to the keyboard. Checking if the height changed by 60 or more will make sure that they keyboard really changed its open state.
409
+ if (Math.abs(previousDiffFromInitial.current - diffFromInitial) > 60) {
410
+ keyboardIsOpen.current = !keyboardIsOpen.current;
411
+ }
412
+ if (snapPoints && snapPoints.length > 0 && snapPointsOffset && activeSnapPointIndex) {
413
+ const activeSnapPointHeight = snapPointsOffset[activeSnapPointIndex] || 0;
414
+ diffFromInitial += activeSnapPointHeight;
415
+ }
416
+ previousDiffFromInitial.current = diffFromInitial;
417
+ // We don't have to change the height if the input is in view, when we are here we are in the opened keyboard state so we can correctly check if the input is in view
418
+ if (drawerHeight > visualViewportHeight || keyboardIsOpen.current) {
419
+ const height = drawerRef.current.getBoundingClientRect().height;
420
+ let newDrawerHeight = height;
421
+ if (height > visualViewportHeight) {
422
+ newDrawerHeight = visualViewportHeight - (isTallEnough ? offsetFromTop : WINDOW_TOP_OFFSET);
423
+ }
424
+ // When fixed, don't move the drawer upwards if there's space, but rather only change it's height so it's fully scrollable when the keyboard is open
425
+ if (fixed) {
426
+ drawerRef.current.style.height = `${height - Math.max(diffFromInitial, 0)}px`;
427
+ } else {
428
+ drawerRef.current.style.height = `${Math.max(newDrawerHeight, visualViewportHeight - offsetFromTop)}px`;
429
+ }
430
+ } else if (!isMobileFirefox()) {
431
+ drawerRef.current.style.height = `${initialDrawerHeight.current}px`;
432
+ }
433
+ if (snapPoints && snapPoints.length > 0 && !keyboardIsOpen.current) {
434
+ drawerRef.current.style.bottom = `0px`;
435
+ } else {
436
+ // Negative bottom value would never make sense
437
+ drawerRef.current.style.bottom = `${Math.max(diffFromInitial, 0)}px`;
438
+ }
439
+ }
440
+ }
441
+ window.visualViewport?.addEventListener('resize', onVisualViewportChange);
442
+ return () => window.visualViewport?.removeEventListener('resize', onVisualViewportChange);
443
+ }, [activeSnapPointIndex, snapPoints, snapPointsOffset]);
444
+ function closeDrawer(fromWithin) {
445
+ cancelDrag();
446
+ onClose?.();
447
+ if (!fromWithin) {
448
+ setIsOpen(false);
449
+ }
450
+ }
451
+ function resetDrawer() {
452
+ if (!drawerRef.current) return;
453
+ const wrapper = document.querySelector('[data-vaul-drawer-wrapper]');
454
+ const currentSwipeAmount = getTranslate(drawerRef.current, direction);
455
+ set(drawerRef.current, {
456
+ transform: 'translate3d(0, 0, 0)',
457
+ transition: `transform ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`
458
+ });
459
+ set(overlayRef.current, {
460
+ transition: `opacity ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`,
461
+ opacity: '1'
462
+ });
463
+
464
+ // Don't reset background if swiped upwards
465
+ if (shouldScaleBackground && currentSwipeAmount && currentSwipeAmount > 0 && isOpen) {
466
+ set(wrapper, {
467
+ borderRadius: `${BORDER_RADIUS}px`,
468
+ overflow: 'hidden',
469
+ ...(isVertical(direction) ? {
470
+ transform: `scale(${getScale()}) translate3d(0, calc(env(safe-area-inset-top) + 14px), 0)`,
471
+ transformOrigin: 'top'
472
+ } : {
473
+ transform: `scale(${getScale()}) translate3d(calc(env(safe-area-inset-top) + 14px), 0, 0)`,
474
+ transformOrigin: 'left'
475
+ }),
476
+ transitionProperty: 'transform, border-radius',
477
+ transitionDuration: `${TRANSITIONS.DURATION}s`,
478
+ transitionTimingFunction: `cubic-bezier(${TRANSITIONS.EASE.join(',')})`
479
+ }, true);
480
+ }
481
+ }
482
+ function cancelDrag() {
483
+ if (!isDragging || !drawerRef.current) return;
484
+ drawerRef.current.classList.remove(DRAG_CLASS);
485
+ isAllowedToDrag.current = false;
486
+ setIsDragging(false);
487
+ dragEndTime.current = new Date();
488
+ }
489
+ function onRelease(event) {
490
+ if (!isDragging || !drawerRef.current) return;
491
+ drawerRef.current.classList.remove(DRAG_CLASS);
492
+ isAllowedToDrag.current = false;
493
+ setIsDragging(false);
494
+ dragEndTime.current = new Date();
495
+ const swipeAmount = getTranslate(drawerRef.current, direction);
496
+ if (!event || !shouldDrag(event.target, false) || !swipeAmount || Number.isNaN(swipeAmount)) return;
497
+ if (dragStartTime.current === null) return;
498
+ const timeTaken = dragEndTime.current.getTime() - dragStartTime.current.getTime();
499
+ const distMoved = pointerStart.current - (isVertical(direction) ? event.pageY : event.pageX);
500
+ const velocity = Math.abs(distMoved) / timeTaken;
501
+ if (velocity > 0.05) {
502
+ // `justReleased` is needed to prevent the drawer from focusing on an input when the drag ends, as it's not the intent most of the time.
503
+ setJustReleased(true);
504
+ setTimeout(() => {
505
+ setJustReleased(false);
506
+ }, 200);
507
+ }
508
+ if (snapPoints) {
509
+ const directionMultiplier = direction === 'bottom' || direction === 'right' ? 1 : -1;
510
+ onReleaseSnapPoints({
511
+ draggedDistance: distMoved * directionMultiplier,
512
+ closeDrawer,
513
+ velocity,
514
+ dismissible
515
+ });
516
+ onReleaseProp?.(event, true);
517
+ return;
518
+ }
519
+
520
+ // Moved upwards, don't do anything
521
+ if (direction === 'bottom' || direction === 'right' ? distMoved > 0 : distMoved < 0) {
522
+ resetDrawer();
523
+ onReleaseProp?.(event, true);
524
+ return;
525
+ }
526
+ if (velocity > VELOCITY_THRESHOLD) {
527
+ closeDrawer();
528
+ onReleaseProp?.(event, false);
529
+ return;
530
+ }
531
+ const visibleDrawerHeight = Math.min(drawerRef.current.getBoundingClientRect().height ?? 0, window.innerHeight);
532
+ const visibleDrawerWidth = Math.min(drawerRef.current.getBoundingClientRect().width ?? 0, window.innerWidth);
533
+ const isHorizontalSwipe = direction === 'left' || direction === 'right';
534
+ if (Math.abs(swipeAmount) >= (isHorizontalSwipe ? visibleDrawerWidth : visibleDrawerHeight) * closeThreshold) {
535
+ closeDrawer();
536
+ onReleaseProp?.(event, false);
537
+ return;
538
+ }
539
+ onReleaseProp?.(event, true);
540
+ resetDrawer();
541
+ }
542
+ React.useEffect(() => {
543
+ // Trigger enter animation without using CSS animation
544
+ if (isOpen) {
545
+ set(document.documentElement, {
546
+ scrollBehavior: 'auto'
547
+ });
548
+ openTime.current = new Date();
549
+ }
550
+ return () => {
551
+ reset(document.documentElement, 'scrollBehavior');
552
+ };
553
+ }, [isOpen]);
554
+ function onNestedOpenChange(o) {
555
+ const scale = o ? (window.innerWidth - NESTED_DISPLACEMENT) / window.innerWidth : 1;
556
+ const initialTranslate = o ? -NESTED_DISPLACEMENT : 0;
557
+ if (nestedOpenChangeTimer.current) {
558
+ window.clearTimeout(nestedOpenChangeTimer.current);
559
+ }
560
+ set(drawerRef.current, {
561
+ transition: `transform ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`,
562
+ transform: isVertical(direction) ? `scale(${scale}) translate3d(0, ${initialTranslate}px, 0)` : `scale(${scale}) translate3d(${initialTranslate}px, 0, 0)`
563
+ });
564
+ if (!o && drawerRef.current) {
565
+ nestedOpenChangeTimer.current = setTimeout(() => {
566
+ const translateValue = getTranslate(drawerRef.current, direction);
567
+ set(drawerRef.current, {
568
+ transition: 'none',
569
+ transform: isVertical(direction) ? `translate3d(0, ${translateValue}px, 0)` : `translate3d(${translateValue}px, 0, 0)`
570
+ });
571
+ }, 500);
572
+ }
573
+ }
574
+ function onNestedDrag(_event, percentageDragged) {
575
+ if (percentageDragged < 0) return;
576
+ const initialScale = (window.innerWidth - NESTED_DISPLACEMENT) / window.innerWidth;
577
+ const newScale = initialScale + percentageDragged * (1 - initialScale);
578
+ const newTranslate = -NESTED_DISPLACEMENT + percentageDragged * NESTED_DISPLACEMENT;
579
+ set(drawerRef.current, {
580
+ transform: isVertical(direction) ? `scale(${newScale}) translate3d(0, ${newTranslate}px, 0)` : `scale(${newScale}) translate3d(${newTranslate}px, 0, 0)`,
581
+ transition: 'none'
582
+ });
583
+ }
584
+ function onNestedRelease(_event, o) {
585
+ const dim = isVertical(direction) ? window.innerHeight : window.innerWidth;
586
+ const scale = o ? (dim - NESTED_DISPLACEMENT) / dim : 1;
587
+ const translate = o ? -NESTED_DISPLACEMENT : 0;
588
+ if (o) {
589
+ set(drawerRef.current, {
590
+ transition: `transform ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`,
591
+ transform: isVertical(direction) ? `scale(${scale}) translate3d(0, ${translate}px, 0)` : `scale(${scale}) translate3d(${translate}px, 0, 0)`
592
+ });
593
+ }
594
+ }
595
+ React.useEffect(() => {
596
+ if (!modal) {
597
+ // Need to do this manually unfortunately
598
+ window.requestAnimationFrame(() => {
599
+ document.body.style.pointerEvents = 'auto';
600
+ });
601
+ }
602
+ }, [modal]);
603
+ return /*#__PURE__*/_jsx(DialogPrimitive.Root, {
604
+ defaultOpen: defaultOpen,
605
+ onOpenChange: open => {
606
+ if (!dismissible && !open) return;
607
+ if (open) {
608
+ setHasBeenOpened(true);
609
+ } else {
610
+ closeDrawer(true);
611
+ }
612
+ setIsOpen(open);
613
+ },
614
+ open: isOpen,
615
+ modal: modal,
616
+ children: /*#__PURE__*/_jsx(DrawerContext.Provider, {
617
+ value: {
618
+ activeSnapPoint,
619
+ snapPoints,
620
+ setActiveSnapPoint,
621
+ drawerRef,
622
+ overlayRef,
623
+ onOpenChange,
624
+ onPress,
625
+ onRelease,
626
+ onDrag,
627
+ dismissible,
628
+ shouldAnimate,
629
+ handleOnly,
630
+ isOpen,
631
+ isDragging,
632
+ shouldFade,
633
+ closeDrawer,
634
+ onNestedDrag,
635
+ onNestedOpenChange,
636
+ onNestedRelease,
637
+ keyboardIsOpen,
638
+ modal,
639
+ snapPointsOffset,
640
+ activeSnapPointIndex,
641
+ fadeFromIndex,
642
+ direction,
643
+ shouldScaleBackground,
644
+ setBackgroundColorOnScale,
645
+ noBodyStyles,
646
+ container,
647
+ autoFocus,
648
+ onPositionChangeRef,
649
+ setContentHeight,
650
+ detached,
651
+ detachedOffset: detached ? detachedOffset : 0,
652
+ detachedRadius: detached ? detachedRadius : 0,
653
+ detachedWrapperStyle
654
+ },
655
+ children: children
656
+ })
657
+ });
658
+ }
659
+ export const Overlay = /*#__PURE__*/React.forwardRef(function ({
660
+ style,
661
+ ...rest
662
+ }, ref) {
663
+ const {
664
+ overlayRef,
665
+ snapPoints,
666
+ onRelease,
667
+ shouldFade,
668
+ isOpen,
669
+ modal,
670
+ shouldAnimate,
671
+ activeSnapPointIndex,
672
+ fadeFromIndex
673
+ } = useDrawerContext();
674
+ const composedRef = useComposedRefs(ref, overlayRef);
675
+ const hasSnapPoints = snapPoints && snapPoints.length > 0;
676
+ const [delayedSnapPoints, setDelayedSnapPoints] = React.useState(false);
677
+ const onMouseUp = React.useCallback(event => onRelease(event), [onRelease]);
678
+ React.useEffect(() => {
679
+ if (hasSnapPoints) {
680
+ window.requestAnimationFrame(() => {
681
+ setDelayedSnapPoints(true);
682
+ });
683
+ }
684
+ }, []);
685
+
686
+ // Overlay is the component that is locking scroll, removing it will unlock the scroll without having to dig into Radix's Dialog library
687
+ if (!modal) {
688
+ return null;
689
+ }
690
+
691
+ // When the active snap is below fadeFromIndex, the overlay is fully
692
+ // transparent (CSS opacity: 0). Radix's DialogOverlayImpl sets inline
693
+ // `pointer-events: auto` which would still catch clicks on the invisible
694
+ // layer — drive this via React so it stays correct even before
695
+ // `snapToPoint` runs (Presence defers the drawer/overlay mount, leaving
696
+ // refs null on the first open effect pass).
697
+ const isBelowFade = hasSnapPoints && fadeFromIndex !== undefined && typeof activeSnapPointIndex === 'number' && activeSnapPointIndex < fadeFromIndex;
698
+ return /*#__PURE__*/_jsx(DialogPrimitive.Overlay, {
699
+ onMouseUp: onMouseUp,
700
+ ref: composedRef,
701
+ "data-vaul-overlay": "",
702
+ "data-vaul-snap-points": isOpen && hasSnapPoints ? 'true' : 'false',
703
+ "data-vaul-delayed-snap-points": delayedSnapPoints ? 'true' : 'false',
704
+ "data-vaul-snap-points-overlay": isOpen && shouldFade ? 'true' : 'false',
705
+ "data-vaul-animate": shouldAnimate?.current ? 'true' : 'false',
706
+ ...rest,
707
+ style: isBelowFade ? overlayBelowFadeStyle(style) : style
708
+ });
709
+ });
710
+ Overlay.displayName = 'Drawer.Overlay';
711
+ const overlayPointerEventsNone = {
712
+ pointerEvents: 'none'
713
+ };
714
+ const overlayBelowFadeStyle = style => style ? {
715
+ ...style,
716
+ ...overlayPointerEventsNone
717
+ } : overlayPointerEventsNone;
718
+ export const Content = /*#__PURE__*/React.forwardRef(function ({
719
+ onPointerDownOutside,
720
+ style,
721
+ onOpenAutoFocus,
722
+ children,
723
+ detachedSiblings,
724
+ ...rest
725
+ }, ref) {
726
+ const {
727
+ drawerRef,
728
+ onPress,
729
+ onRelease,
730
+ onDrag,
731
+ keyboardIsOpen,
732
+ snapPointsOffset,
733
+ activeSnapPointIndex,
734
+ fadeFromIndex,
735
+ modal,
736
+ isOpen,
737
+ isDragging,
738
+ direction,
739
+ snapPoints,
740
+ container,
741
+ handleOnly,
742
+ shouldAnimate,
743
+ autoFocus,
744
+ onPositionChangeRef,
745
+ setContentHeight,
746
+ detached,
747
+ detachedOffset,
748
+ detachedRadius,
749
+ detachedWrapperStyle: detachedWrapperStyleProp
750
+ } = useDrawerContext();
751
+ const hasAutoSnapPoint = React.useMemo(() => !!snapPoints?.some(p => p === 'auto'), [snapPoints]);
752
+
753
+ // When 'auto' is used as a snap point, we need the natural content height.
754
+ // The drawer itself may be styled to a fixed viewport height, so we measure
755
+ // an inner wrapper instead. Ref callback starts the observer as soon as the
756
+ // node mounts (Radix Presence defers the portal mount past useEffect).
757
+ const autoRoRef = React.useRef(null);
758
+ const setAutoSizeNode = React.useCallback(node => {
759
+ autoRoRef.current?.disconnect();
760
+ if (!node || !hasAutoSnapPoint) {
761
+ autoRoRef.current = null;
762
+ return;
763
+ }
764
+ const measure = () => setContentHeight(node.offsetHeight);
765
+ measure();
766
+ const ro = new ResizeObserver(measure);
767
+ ro.observe(node);
768
+ autoRoRef.current = ro;
769
+ }, [hasAutoSnapPoint, setContentHeight]);
770
+ const isBelowFade = snapPoints !== undefined && snapPoints !== null && fadeFromIndex !== undefined && typeof activeSnapPointIndex === 'number' && activeSnapPointIndex < fadeFromIndex;
771
+ // Needed to use transition instead of animations
772
+ const [delayedSnapPoints, setDelayedSnapPoints] = React.useState(false);
773
+ const composedRef = useComposedRefs(ref, drawerRef);
774
+ const pointerStartRef = React.useRef(null);
775
+ const lastKnownPointerEventRef = React.useRef(null);
776
+ const wasBeyondThePointRef = React.useRef(false);
777
+ const hasSnapPoints = snapPoints && snapPoints.length > 0;
778
+ useScaleBackground();
779
+ const isDeltaInDirection = (delta, dir, threshold = 0) => {
780
+ if (wasBeyondThePointRef.current) return true;
781
+ const deltaY = Math.abs(delta.y);
782
+ const deltaX = Math.abs(delta.x);
783
+ const isDeltaX = deltaX > deltaY;
784
+ const dFactor = ['bottom', 'right'].includes(dir) ? 1 : -1;
785
+ if (dir === 'left' || dir === 'right') {
786
+ const isReverseDirection = delta.x * dFactor < 0;
787
+ if (!isReverseDirection && deltaX >= 0 && deltaX <= threshold) {
788
+ return isDeltaX;
789
+ }
790
+ } else {
791
+ const isReverseDirection = delta.y * dFactor < 0;
792
+ if (!isReverseDirection && deltaY >= 0 && deltaY <= threshold) {
793
+ return !isDeltaX;
794
+ }
795
+ }
796
+ wasBeyondThePointRef.current = true;
797
+ return true;
798
+ };
799
+ React.useEffect(() => {
800
+ if (hasSnapPoints) {
801
+ window.requestAnimationFrame(() => {
802
+ setDelayedSnapPoints(true);
803
+ });
804
+ }
805
+ }, []);
806
+
807
+ // Event-driven position tracking. We only tick RAF while the drawer is
808
+ // actually moving (drag / CSS transition / CSS animation). When it's idle at
809
+ // a snap, no frames run at all.
810
+ const positionTrackingRef = React.useRef(null);
811
+ React.useEffect(() => {
812
+ const drawer = drawerRef.current;
813
+ if (!drawer) return;
814
+ const state = {
815
+ rafId: null,
816
+ movingCount: 0,
817
+ lastPosition: Number.NaN,
818
+ start: () => {},
819
+ stop: () => {}
820
+ };
821
+ const emit = () => {
822
+ const cb = onPositionChangeRef.current;
823
+ if (!cb) return;
824
+ const position = drawer.getBoundingClientRect().top;
825
+ if (position !== state.lastPosition) {
826
+ state.lastPosition = position;
827
+ cb(position);
828
+ }
829
+ };
830
+ const tick = () => {
831
+ emit();
832
+ state.rafId = state.movingCount > 0 ? window.requestAnimationFrame(tick) : null;
833
+ };
834
+ state.start = () => {
835
+ state.movingCount += 1;
836
+ if (state.rafId === null) {
837
+ state.rafId = window.requestAnimationFrame(tick);
838
+ }
839
+ };
840
+ state.stop = () => {
841
+ state.movingCount = Math.max(0, state.movingCount - 1);
842
+ // RAF loop will exit on its next tick; emit the settled position now.
843
+ if (state.movingCount === 0) emit();
844
+ };
845
+ const wrapper = drawer.closest('[data-vaul-detached-wrapper]');
846
+
847
+ // Listen on the wrapper too: drag-overshoot snap-back animates the wrapper
848
+ // alone when the drawer's target is unchanged (e.g. snapping back to the
849
+ // same detent). Without this, position goes stale mid-animation because
850
+ // the drawer's `transitionrun` never fires.
851
+ const onTransitionRun = e => {
852
+ if ((e.target === drawer || e.target === wrapper) && e.propertyName === 'transform') state.start();
853
+ };
854
+ const onTransitionDone = e => {
855
+ if ((e.target === drawer || e.target === wrapper) && e.propertyName === 'transform') state.stop();
856
+ };
857
+ const onAnimationStart = e => {
858
+ if (e.target === drawer || e.target === wrapper) state.start();
859
+ };
860
+ const onAnimationDone = e => {
861
+ if (e.target === drawer || e.target === wrapper) state.stop();
862
+ };
863
+ drawer.addEventListener('transitionrun', onTransitionRun);
864
+ drawer.addEventListener('transitionend', onTransitionDone);
865
+ drawer.addEventListener('transitioncancel', onTransitionDone);
866
+ drawer.addEventListener('animationstart', onAnimationStart);
867
+ drawer.addEventListener('animationend', onAnimationDone);
868
+ drawer.addEventListener('animationcancel', onAnimationDone);
869
+ wrapper?.addEventListener('transitionrun', onTransitionRun);
870
+ wrapper?.addEventListener('transitionend', onTransitionDone);
871
+ wrapper?.addEventListener('transitioncancel', onTransitionDone);
872
+ positionTrackingRef.current = state;
873
+ emit();
874
+ return () => {
875
+ drawer.removeEventListener('transitionrun', onTransitionRun);
876
+ drawer.removeEventListener('transitionend', onTransitionDone);
877
+ drawer.removeEventListener('transitioncancel', onTransitionDone);
878
+ drawer.removeEventListener('animationstart', onAnimationStart);
879
+ drawer.removeEventListener('animationend', onAnimationDone);
880
+ drawer.removeEventListener('animationcancel', onAnimationDone);
881
+ wrapper?.removeEventListener('transitionrun', onTransitionRun);
882
+ wrapper?.removeEventListener('transitionend', onTransitionDone);
883
+ wrapper?.removeEventListener('transitioncancel', onTransitionDone);
884
+ if (state.rafId !== null) window.cancelAnimationFrame(state.rafId);
885
+ positionTrackingRef.current = null;
886
+ };
887
+ }, []);
888
+ React.useEffect(() => {
889
+ const state = positionTrackingRef.current;
890
+ if (!state) return;
891
+ if (isDragging) state.start();else state.stop();
892
+ }, [isDragging]);
893
+ function handleOnPointerUp(event) {
894
+ pointerStartRef.current = null;
895
+ wasBeyondThePointRef.current = false;
896
+ onRelease(event);
897
+ }
898
+
899
+ // The drawer always sits inside a fixed clip wrapper. `contain: paint`
900
+ // establishes the wrapper as the containing block so the drawer's own
901
+ // `position: fixed` is constrained here, and `overflow: hidden` keeps any
902
+ // overshoot out of the viewport. When `detached` the wrapper also floats
903
+ // with a bottom gap and rounded bottom corners; otherwise it sits flush.
904
+ // Transform/transition are managed imperatively (via drag overshoot and the
905
+ // dismiss effect) so React doesn't skip DOM writes for values it thinks it
906
+ // already owns.
907
+ const wrapperStyle = React.useMemo(() => ({
908
+ position: 'fixed',
909
+ top: 0,
910
+ left: 0,
911
+ right: 0,
912
+ bottom: detached ? detachedOffset : 0,
913
+ overflow: 'hidden',
914
+ contain: 'paint',
915
+ pointerEvents: 'none',
916
+ borderBottomLeftRadius: detached ? detachedRadius : 0,
917
+ borderBottomRightRadius: detached ? detachedRadius : 0,
918
+ ...detachedWrapperStyleProp
919
+ }), [detached, detachedOffset, detachedRadius, detachedWrapperStyleProp]);
920
+
921
+ // Translate the wrapper off-screen on dismiss so the whole card slides out
922
+ // as one. The reset-then-target pattern on open forces the browser to
923
+ // record a starting value so the transition actually animates.
924
+ const wasOpenRef = React.useRef(isOpen);
925
+ React.useEffect(() => {
926
+ if (!drawerRef.current) {
927
+ wasOpenRef.current = isOpen;
928
+ return;
929
+ }
930
+ const wrapper = drawerRef.current.closest('[data-vaul-detached-wrapper]');
931
+ if (wrapper) {
932
+ const transition = `transform ${TRANSITIONS.DURATION}s cubic-bezier(${TRANSITIONS.EASE.join(',')})`;
933
+ const viewportH = typeof window !== 'undefined' ? window.innerHeight : 0;
934
+ if (!isOpen && wasOpenRef.current) {
935
+ wrapper.style.transition = transition;
936
+ wrapper.style.transform = `translate3d(0, ${viewportH}px, 0)`;
937
+ } else if (isOpen && !wasOpenRef.current) {
938
+ wrapper.style.transition = 'none';
939
+ wrapper.style.transform = `translate3d(0, ${viewportH}px, 0)`;
940
+ // eslint-disable-next-line no-void
941
+ void wrapper.offsetHeight;
942
+ wrapper.style.transition = transition;
943
+ wrapper.style.transform = 'translate3d(0, 0, 0)';
944
+ } else if (isOpen && !wrapper.style.transform) {
945
+ // Fresh mount with open=true — ensure the wrapper starts at rest.
946
+ wrapper.style.transition = transition;
947
+ wrapper.style.transform = 'translate3d(0, 0, 0)';
948
+ }
949
+ }
950
+ wasOpenRef.current = isOpen;
951
+ }, [isOpen, detached, drawerRef]);
952
+ const contentNode = /*#__PURE__*/_jsx(DialogPrimitive.Content, {
953
+ "data-vaul-drawer-direction": direction,
954
+ "data-vaul-drawer": "",
955
+ "data-vaul-detached": detached ? 'true' : 'false',
956
+ "data-vaul-delayed-snap-points": delayedSnapPoints ? 'true' : 'false',
957
+ "data-vaul-snap-points": isOpen && hasSnapPoints ? 'true' : 'false',
958
+ "data-vaul-custom-container": container ? 'true' : 'false',
959
+ "data-vaul-animate": shouldAnimate?.current ? 'true' : 'false',
960
+ ...rest,
961
+ ref: composedRef,
962
+ style: snapPointsOffset && snapPointsOffset.length > 0 ? {
963
+ '--snap-point-height': `${snapPointsOffset[activeSnapPointIndex ?? 0]}px`,
964
+ ...style,
965
+ 'pointerEvents': 'auto'
966
+ } : {
967
+ ...style,
968
+ pointerEvents: 'auto'
969
+ },
970
+ onPointerDown: event => {
971
+ if (handleOnly) return;
972
+ rest.onPointerDown?.(event);
973
+ pointerStartRef.current = {
974
+ x: event.pageX,
975
+ y: event.pageY
976
+ };
977
+ onPress(event);
978
+ },
979
+ onOpenAutoFocus: e => {
980
+ onOpenAutoFocus?.(e);
981
+ if (!autoFocus) {
982
+ e.preventDefault();
983
+ }
984
+ },
985
+ onPointerDownOutside: e => {
986
+ onPointerDownOutside?.(e);
987
+ if (!modal || e.defaultPrevented || isBelowFade) {
988
+ e.preventDefault();
989
+ return;
990
+ }
991
+ if (keyboardIsOpen.current) {
992
+ keyboardIsOpen.current = false;
993
+ }
994
+ },
995
+ onFocusOutside: e => {
996
+ if (!modal || isBelowFade) {
997
+ e.preventDefault();
998
+ return;
999
+ }
1000
+ },
1001
+ onPointerMove: event => {
1002
+ lastKnownPointerEventRef.current = event;
1003
+ if (handleOnly) return;
1004
+ rest.onPointerMove?.(event);
1005
+ if (!pointerStartRef.current) return;
1006
+ const yPosition = event.pageY - pointerStartRef.current.y;
1007
+ const xPosition = event.pageX - pointerStartRef.current.x;
1008
+ const swipeStartThreshold = event.pointerType === 'touch' ? 10 : 2;
1009
+ const delta = {
1010
+ x: xPosition,
1011
+ y: yPosition
1012
+ };
1013
+ const isAllowedToSwipe = isDeltaInDirection(delta, direction, swipeStartThreshold);
1014
+ if (isAllowedToSwipe) onDrag(event);else if (Math.abs(xPosition) > swipeStartThreshold || Math.abs(yPosition) > swipeStartThreshold) {
1015
+ pointerStartRef.current = null;
1016
+ }
1017
+ },
1018
+ onPointerUp: event => {
1019
+ rest.onPointerUp?.(event);
1020
+ pointerStartRef.current = null;
1021
+ wasBeyondThePointRef.current = false;
1022
+ onRelease(event);
1023
+ },
1024
+ onPointerOut: event => {
1025
+ rest.onPointerOut?.(event);
1026
+ handleOnPointerUp(lastKnownPointerEventRef.current);
1027
+ },
1028
+ onContextMenu: event => {
1029
+ rest.onContextMenu?.(event);
1030
+ if (lastKnownPointerEventRef.current) {
1031
+ handleOnPointerUp(lastKnownPointerEventRef.current);
1032
+ }
1033
+ },
1034
+ children: hasAutoSnapPoint ? /*#__PURE__*/_jsx("div", {
1035
+ ref: setAutoSizeNode,
1036
+ "data-vaul-auto-size-wrapper": "",
1037
+ style: autoSizeWrapperStyle,
1038
+ children: children
1039
+ }) : children
1040
+ });
1041
+ return /*#__PURE__*/_jsxs("div", {
1042
+ "data-vaul-detached-wrapper": "",
1043
+ style: wrapperStyle,
1044
+ children: [contentNode, detachedSiblings]
1045
+ });
1046
+ });
1047
+ Content.displayName = 'Drawer.Content';
1048
+
1049
+ // `flow-root` establishes a new block formatting context so the first child's
1050
+ // margin-top (e.g. a grabber) stays inside the wrapper instead of collapsing
1051
+ // out — otherwise `offsetHeight` under-reports and the drawer positions the
1052
+ // wrapper below where the content actually ends.
1053
+ const autoSizeWrapperStyle = {
1054
+ display: 'flow-root'
1055
+ };
1056
+ const LONG_HANDLE_PRESS_TIMEOUT = 250;
1057
+ const DOUBLE_TAP_TIMEOUT = 120;
1058
+ export const Handle = /*#__PURE__*/React.forwardRef(function ({
1059
+ preventCycle = false,
1060
+ children,
1061
+ ...rest
1062
+ }, ref) {
1063
+ const {
1064
+ closeDrawer,
1065
+ isDragging,
1066
+ snapPoints,
1067
+ activeSnapPoint,
1068
+ setActiveSnapPoint,
1069
+ dismissible,
1070
+ handleOnly,
1071
+ isOpen,
1072
+ onPress,
1073
+ onDrag
1074
+ } = useDrawerContext();
1075
+ const closeTimeoutIdRef = React.useRef(null);
1076
+ const shouldCancelInteractionRef = React.useRef(false);
1077
+ function handleStartCycle() {
1078
+ // Stop if this is the second click of a double click
1079
+ if (shouldCancelInteractionRef.current) {
1080
+ handleCancelInteraction();
1081
+ return;
1082
+ }
1083
+ window.setTimeout(() => {
1084
+ handleCycleSnapPoints();
1085
+ }, DOUBLE_TAP_TIMEOUT);
1086
+ }
1087
+ function handleCycleSnapPoints() {
1088
+ // Prevent accidental taps while resizing drawer
1089
+ if (isDragging || preventCycle || shouldCancelInteractionRef.current) {
1090
+ handleCancelInteraction();
1091
+ return;
1092
+ }
1093
+ // Make sure to clear the timeout id if the user releases the handle before the cancel timeout
1094
+ handleCancelInteraction();
1095
+ if (!snapPoints || snapPoints.length === 0) {
1096
+ if (!dismissible) {
1097
+ closeDrawer();
1098
+ }
1099
+ return;
1100
+ }
1101
+ const isLastSnapPoint = activeSnapPoint === snapPoints[snapPoints.length - 1];
1102
+ if (isLastSnapPoint && dismissible) {
1103
+ closeDrawer();
1104
+ return;
1105
+ }
1106
+ const currentSnapIndex = snapPoints.findIndex(point => point === activeSnapPoint);
1107
+ if (currentSnapIndex === -1) return; // activeSnapPoint not found in snapPoints
1108
+ const nextSnapPoint = snapPoints[currentSnapIndex + 1];
1109
+ if (nextSnapPoint === undefined) return;
1110
+ setActiveSnapPoint(nextSnapPoint);
1111
+ }
1112
+ function handleStartInteraction() {
1113
+ closeTimeoutIdRef.current = window.setTimeout(() => {
1114
+ // Cancel click interaction on a long press
1115
+ shouldCancelInteractionRef.current = true;
1116
+ }, LONG_HANDLE_PRESS_TIMEOUT);
1117
+ }
1118
+ function handleCancelInteraction() {
1119
+ if (closeTimeoutIdRef.current) {
1120
+ window.clearTimeout(closeTimeoutIdRef.current);
1121
+ }
1122
+ shouldCancelInteractionRef.current = false;
1123
+ }
1124
+ return /*#__PURE__*/_jsx("div", {
1125
+ onClick: handleStartCycle,
1126
+ onPointerCancel: handleCancelInteraction,
1127
+ onPointerDown: e => {
1128
+ if (handleOnly) onPress(e);
1129
+ handleStartInteraction();
1130
+ },
1131
+ onPointerMove: e => {
1132
+ if (handleOnly) onDrag(e);
1133
+ }
1134
+ // onPointerUp is already handled by the content component
1135
+ ,
1136
+ ref: ref,
1137
+ "data-vaul-drawer-visible": isOpen ? 'true' : 'false',
1138
+ "data-vaul-handle": "",
1139
+ "aria-hidden": "true",
1140
+ ...rest,
1141
+ children: /*#__PURE__*/_jsx("span", {
1142
+ "data-vaul-handle-hitarea": "",
1143
+ "aria-hidden": "true",
1144
+ children: children
1145
+ })
1146
+ });
1147
+ });
1148
+ Handle.displayName = 'Drawer.Handle';
1149
+ export function NestedRoot({
1150
+ onDrag,
1151
+ onOpenChange,
1152
+ open: nestedIsOpen,
1153
+ ...rest
1154
+ }) {
1155
+ const {
1156
+ onNestedDrag,
1157
+ onNestedOpenChange,
1158
+ onNestedRelease
1159
+ } = useDrawerContext();
1160
+ if (!onNestedDrag) {
1161
+ throw new Error('Drawer.NestedRoot must be placed in another drawer');
1162
+ }
1163
+ return /*#__PURE__*/_jsx(Root, {
1164
+ nested: true,
1165
+ open: nestedIsOpen,
1166
+ onClose: () => {
1167
+ onNestedOpenChange(false);
1168
+ },
1169
+ onDrag: (e, p) => {
1170
+ onNestedDrag(e, p);
1171
+ onDrag?.(e, p);
1172
+ },
1173
+ onOpenChange: o => {
1174
+ if (o) {
1175
+ onNestedOpenChange(o);
1176
+ }
1177
+ onOpenChange?.(o);
1178
+ },
1179
+ onRelease: onNestedRelease,
1180
+ ...rest
1181
+ });
1182
+ }
1183
+ export function Portal(props) {
1184
+ const context = useDrawerContext();
1185
+ const {
1186
+ container = context.container,
1187
+ ...portalProps
1188
+ } = props;
1189
+ return /*#__PURE__*/_jsx(DialogPrimitive.Portal, {
1190
+ container: container,
1191
+ ...portalProps
1192
+ });
1193
+ }
1194
+ export const Drawer = {
1195
+ Root,
1196
+ NestedRoot,
1197
+ Content,
1198
+ Overlay,
1199
+ Trigger: DialogPrimitive.Trigger,
1200
+ Portal,
1201
+ Handle,
1202
+ Close: DialogPrimitive.Close,
1203
+ Title: DialogPrimitive.Title,
1204
+ Description: DialogPrimitive.Description
1205
+ };
1206
+ //# sourceMappingURL=index.js.map