@jobber/components 9.5.3 → 9.6.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 (178) hide show
  1. package/Combobox.d.ts +1 -0
  2. package/Combobox.js +17 -0
  3. package/dist/Autocomplete/index.cjs +0 -2
  4. package/dist/Autocomplete/index.mjs +0 -2
  5. package/dist/Autocomplete-cjs.js +4 -5
  6. package/dist/Autocomplete-es.js +1 -2
  7. package/dist/Card/index.cjs +2 -2
  8. package/dist/Card/index.mjs +2 -2
  9. package/dist/Chip/index.cjs +0 -2
  10. package/dist/Chip/index.mjs +0 -2
  11. package/dist/Chips/InternalChipDismissible/hooks/index.cjs +0 -2
  12. package/dist/Chips/InternalChipDismissible/hooks/index.mjs +0 -2
  13. package/dist/Chips/InternalChipDismissible/index.cjs +0 -2
  14. package/dist/Chips/InternalChipDismissible/index.mjs +0 -2
  15. package/dist/Chips/index.cjs +0 -2
  16. package/dist/Chips/index.mjs +0 -2
  17. package/dist/Combobox/Combobox.contentSlots.d.ts +5 -0
  18. package/dist/Combobox/Combobox.context.d.ts +34 -0
  19. package/dist/Combobox/Combobox.d.ts +69 -0
  20. package/dist/Combobox/Combobox.footer.d.ts +7 -0
  21. package/dist/Combobox/Combobox.items.d.ts +25 -0
  22. package/dist/Combobox/Combobox.responsive.d.ts +54 -0
  23. package/dist/Combobox/Combobox.selectAll.d.ts +4 -0
  24. package/dist/Combobox/Combobox.slotFactory.d.ts +7 -0
  25. package/dist/Combobox/Combobox.testUtils.d.ts +12 -0
  26. package/dist/Combobox/Combobox.types.d.ts +166 -0
  27. package/dist/Combobox/index.cjs +35 -0
  28. package/dist/Combobox/index.d.ts +2 -0
  29. package/dist/Combobox/index.mjs +29 -0
  30. package/dist/Combobox-cjs.js +675 -0
  31. package/dist/Combobox-es.js +673 -0
  32. package/dist/ComboboxPrimitive-cjs.js +88 -47
  33. package/dist/ComboboxPrimitive-es.js +39 -23
  34. package/dist/{ComboboxChipRemove-cjs.js → ComboboxSeparator-cjs.js} +501 -455
  35. package/dist/{ComboboxChipRemove-es.js → ComboboxSeparator-es.js} +500 -456
  36. package/dist/ConfirmationModal/index.cjs +0 -2
  37. package/dist/ConfirmationModal/index.mjs +0 -2
  38. package/dist/ContentBlock/index.cjs +1 -0
  39. package/dist/ContentBlock/index.mjs +1 -0
  40. package/dist/ContentBlock-cjs.js +2 -18
  41. package/dist/ContentBlock-es.js +1 -17
  42. package/dist/DataDump/index.cjs +2 -2
  43. package/dist/DataDump/index.mjs +2 -2
  44. package/dist/DataList/components/DataListActions/index.cjs +0 -2
  45. package/dist/DataList/components/DataListActions/index.mjs +0 -2
  46. package/dist/DataList/components/DataListBulkActions/index.cjs +0 -2
  47. package/dist/DataList/components/DataListBulkActions/index.mjs +0 -2
  48. package/dist/DataList/components/DataListFilters/components/DataListSort/index.cjs +0 -2
  49. package/dist/DataList/components/DataListFilters/components/DataListSort/index.mjs +0 -2
  50. package/dist/DataList/components/DataListFilters/index.cjs +0 -2
  51. package/dist/DataList/components/DataListFilters/index.mjs +0 -2
  52. package/dist/DataList/components/DataListHeader/index.cjs +0 -2
  53. package/dist/DataList/components/DataListHeader/index.mjs +0 -2
  54. package/dist/DataList/components/DataListItem/index.cjs +0 -2
  55. package/dist/DataList/components/DataListItem/index.mjs +0 -2
  56. package/dist/DataList/components/DataListItemActions/index.cjs +0 -2
  57. package/dist/DataList/components/DataListItemActions/index.mjs +0 -2
  58. package/dist/DataList/components/DataListItemActionsOverflow/index.cjs +0 -2
  59. package/dist/DataList/components/DataListItemActionsOverflow/index.mjs +0 -2
  60. package/dist/DataList/components/DataListItems/index.cjs +0 -2
  61. package/dist/DataList/components/DataListItems/index.mjs +0 -2
  62. package/dist/DataList/components/DataListLayout/index.cjs +0 -2
  63. package/dist/DataList/components/DataListLayout/index.mjs +0 -2
  64. package/dist/DataList/components/DataListLayoutActions/index.cjs +0 -2
  65. package/dist/DataList/components/DataListLayoutActions/index.mjs +0 -2
  66. package/dist/DataList/index.cjs +0 -2
  67. package/dist/DataList/index.mjs +0 -2
  68. package/dist/DatePicker/index.cjs +0 -2
  69. package/dist/DatePicker/index.mjs +0 -2
  70. package/dist/DatePicker-cjs.js +4 -5
  71. package/dist/DatePicker-es.js +1 -2
  72. package/dist/Dialog/index.cjs +2 -2
  73. package/dist/Dialog/index.mjs +2 -2
  74. package/dist/DrawerDescription-cjs.js +268 -99
  75. package/dist/DrawerDescription-es.js +270 -103
  76. package/dist/DrawerTitle-cjs.js +2 -6
  77. package/dist/DrawerTitle-es.js +3 -7
  78. package/dist/FieldDescription-cjs.js +184 -181
  79. package/dist/FieldDescription-es.js +187 -184
  80. package/dist/FilterPicker/components/FilterPickerActivator/index.cjs +0 -2
  81. package/dist/FilterPicker/components/FilterPickerActivator/index.mjs +0 -2
  82. package/dist/FilterPicker/components/FilterPickerContent/index.cjs +0 -2
  83. package/dist/FilterPicker/components/FilterPickerContent/index.mjs +0 -2
  84. package/dist/FilterPicker/components/FilterPickerTrigger/index.cjs +0 -2
  85. package/dist/FilterPicker/components/FilterPickerTrigger/index.mjs +0 -2
  86. package/dist/FilterPicker/index.cjs +0 -2
  87. package/dist/FilterPicker/index.mjs +0 -2
  88. package/dist/FilterPickerContent-cjs.js +4 -5
  89. package/dist/FilterPickerContent-es.js +1 -2
  90. package/dist/FormatFile/index.cjs +0 -2
  91. package/dist/FormatFile/index.mjs +0 -2
  92. package/dist/Gallery/index.cjs +0 -2
  93. package/dist/Gallery/index.mjs +0 -2
  94. package/dist/InputDate/index.cjs +0 -2
  95. package/dist/InputDate/index.mjs +0 -2
  96. package/dist/InputNumber/index.cjs +2 -2
  97. package/dist/InputNumber/index.mjs +2 -2
  98. package/dist/InputNumber/types.d.ts +5 -0
  99. package/dist/InputNumber-cjs.js +2 -2
  100. package/dist/InputNumber-es.js +2 -2
  101. package/dist/LightBox/index.cjs +0 -2
  102. package/dist/LightBox/index.mjs +0 -2
  103. package/dist/Menu/index.cjs +2 -2
  104. package/dist/Menu/index.mjs +2 -2
  105. package/dist/Menu-cjs.js +4 -5
  106. package/dist/Menu-es.js +1 -2
  107. package/dist/MenuSubmenuTrigger-cjs.js +206 -164
  108. package/dist/MenuSubmenuTrigger-es.js +194 -152
  109. package/dist/Modal/index.cjs +0 -2
  110. package/dist/Modal/index.mjs +1 -3
  111. package/dist/NumberFieldInput-cjs.js +187 -308
  112. package/dist/NumberFieldInput-es.js +187 -308
  113. package/dist/Page/index.cjs +2 -2
  114. package/dist/Page/index.mjs +2 -2
  115. package/dist/Popover/index.cjs +0 -2
  116. package/dist/Popover/index.mjs +0 -2
  117. package/dist/Popover-cjs.js +7 -8
  118. package/dist/Popover-es.js +1 -2
  119. package/dist/ScrollAreaViewport-cjs.js +747 -1093
  120. package/dist/ScrollAreaViewport-es.js +734 -1081
  121. package/dist/Select/index.cjs +2 -2
  122. package/dist/Select/index.mjs +2 -2
  123. package/dist/SelectGroupLabel-cjs.js +207 -306
  124. package/dist/SelectGroupLabel-es.js +203 -302
  125. package/dist/SelectPrimitive-cjs.js +11 -17
  126. package/dist/SelectPrimitive-es.js +1 -7
  127. package/dist/Tooltip/index.cjs +0 -2
  128. package/dist/Tooltip/index.mjs +0 -2
  129. package/dist/Tooltip-cjs.js +6 -7
  130. package/dist/Tooltip-es.js +1 -2
  131. package/dist/buttonRenderAdapter-cjs.js +2 -1
  132. package/dist/buttonRenderAdapter-es.js +2 -1
  133. package/dist/dialogReturnFocus-cjs.js +1 -1
  134. package/dist/dialogReturnFocus-es.js +2 -2
  135. package/dist/docs/InputNumber/InputNumber.md +2 -2
  136. package/dist/docs/SelectPrimitive/SelectPrimitive.md +1 -1
  137. package/dist/floating-ui.react-cjs.js +2557 -185
  138. package/dist/floating-ui.react-es.js +2512 -149
  139. package/dist/getMappedBreakpointWidth-cjs.js +21 -0
  140. package/dist/getMappedBreakpointWidth-es.js +18 -0
  141. package/dist/index.cjs +15 -10
  142. package/dist/index.d.mts +1 -0
  143. package/dist/index.d.ts +1 -0
  144. package/dist/index.mjs +14 -10
  145. package/dist/mergeClassName-cjs.js +12 -0
  146. package/dist/mergeClassName-es.js +10 -0
  147. package/dist/primitives/BottomSheet/index.cjs +1 -1
  148. package/dist/primitives/BottomSheet/index.mjs +1 -1
  149. package/dist/primitives/ComboboxPrimitive/ComboboxPrimitive.d.ts +2 -2
  150. package/dist/primitives/ComboboxPrimitive/index.cjs +4 -5
  151. package/dist/primitives/ComboboxPrimitive/index.mjs +4 -5
  152. package/dist/primitives/SelectPrimitive/index.cjs +1 -2
  153. package/dist/primitives/SelectPrimitive/index.mjs +1 -2
  154. package/dist/primitives/index.cjs +3 -3
  155. package/dist/primitives/index.mjs +3 -3
  156. package/dist/scrollEdges-cjs.js +1 -11
  157. package/dist/scrollEdges-es.js +1 -11
  158. package/dist/scrollable-cjs.js +58 -0
  159. package/dist/scrollable-es.js +54 -0
  160. package/dist/styles.css +759 -42
  161. package/dist/unstyledPrimitives/index.cjs +720 -478
  162. package/dist/unstyledPrimitives/index.mjs +663 -421
  163. package/dist/useCompositeListItem-cjs.js +2108 -361
  164. package/dist/useCompositeListItem-es.js +2076 -331
  165. package/dist/useLabel-cjs.js +2 -3
  166. package/dist/useLabel-es.js +2 -3
  167. package/dist/useRenderElement-cjs.js +9 -7
  168. package/dist/useRenderElement-es.js +9 -7
  169. package/dist/useScrollLock-cjs.js +819 -344
  170. package/dist/useScrollLock-es.js +732 -296
  171. package/dist/useScrollToActive-cjs.js +4 -5
  172. package/dist/useScrollToActive-es.js +1 -2
  173. package/dist/utils/meta/meta.json +43 -1
  174. package/package.json +8 -3
  175. package/dist/floating-ui.react-dom-cjs.js +0 -2103
  176. package/dist/floating-ui.react-dom-es.js +0 -2072
  177. package/dist/floating-ui.utils-cjs.js +0 -342
  178. package/dist/floating-ui.utils-es.js +0 -300
@@ -5,8 +5,8 @@ var useRenderElement = require('./useRenderElement-cjs.js');
5
5
  var useScrollLock = require('./useScrollLock-cjs.js');
6
6
  var scrollEdges = require('./scrollEdges-cjs.js');
7
7
  var jsxRuntime = require('react/jsx-runtime');
8
- var floatingUi_utils = require('./floating-ui.utils-cjs.js');
9
8
  var ReactDOM = require('react-dom');
9
+ var scrollable = require('./scrollable-cjs.js');
10
10
 
11
11
  function _interopNamespaceDefault(e) {
12
12
  var n = Object.create(null);
@@ -80,12 +80,12 @@ function createPopupFloatingRootContext(triggerElements, floatingId, nested = fa
80
80
  onOpenChange: undefined
81
81
  });
82
82
  }
83
- const activeTriggerIdSelector = useScrollLock.createSelector(state => state.triggerIdProp ?? state.activeTriggerId);
84
- const openSelector = useScrollLock.createSelector(state => state.openProp ?? state.open);
85
- const popupIdSelector = useScrollLock.createSelector(state => {
83
+ const activeTriggerIdSelector = state => state.triggerIdProp ?? state.activeTriggerId;
84
+ const openSelector = state => state.openProp ?? state.open;
85
+ const popupIdSelector = state => {
86
86
  const popupId = state.popupElement?.id ?? state.floatingId;
87
87
  return popupId || undefined;
88
- });
88
+ };
89
89
  function triggerOwnsOpenPopup(state, triggerId) {
90
90
  return triggerId !== undefined && openSelector(state) && activeTriggerIdSelector(state) === triggerId;
91
91
  }
@@ -97,47 +97,81 @@ function triggerOwnsOpenPopupOrIsOnlyTrigger(state, triggerId) {
97
97
  }
98
98
  const popupStoreSelectors = {
99
99
  open: openSelector,
100
- mounted: useScrollLock.createSelector(state => state.mounted),
101
- transitionStatus: useScrollLock.createSelector(state => state.transitionStatus),
102
- floatingRootContext: useScrollLock.createSelector(state => state.floatingRootContext),
103
- triggerCount: useScrollLock.createSelector(state => state.triggerCount),
104
- preventUnmountingOnClose: useScrollLock.createSelector(state => state.preventUnmountingOnClose),
105
- payload: useScrollLock.createSelector(state => state.payload),
100
+ mounted: state => state.mounted,
101
+ transitionStatus: state => state.transitionStatus,
102
+ floatingRootContext: state => state.floatingRootContext,
103
+ triggerCount: state => state.triggerCount,
104
+ preventUnmountingOnClose: state => state.preventUnmountingOnClose,
105
+ payload: state => state.payload,
106
106
  activeTriggerId: activeTriggerIdSelector,
107
- activeTriggerElement: useScrollLock.createSelector(state => state.mounted ? state.activeTriggerElement : null),
107
+ activeTriggerElement: state => state.mounted ? state.activeTriggerElement : null,
108
108
  popupId: popupIdSelector,
109
109
  /**
110
110
  * Whether the trigger with the given ID was used to open the popup.
111
111
  */
112
- isTriggerActive: useScrollLock.createSelector((state, triggerId) => triggerId !== undefined && activeTriggerIdSelector(state) === triggerId),
112
+ isTriggerActive: (state, triggerId) => triggerId !== undefined && activeTriggerIdSelector(state) === triggerId,
113
113
  /**
114
114
  * Whether the popup is open and was activated by a trigger with the given ID.
115
115
  */
116
- isOpenedByTrigger: useScrollLock.createSelector((state, triggerId) => triggerOwnsOpenPopup(state, triggerId)),
116
+ isOpenedByTrigger: (state, triggerId) => triggerOwnsOpenPopup(state, triggerId),
117
117
  /**
118
118
  * Whether the popup is mounted and was activated by a trigger with the given ID.
119
119
  */
120
- isMountedByTrigger: useScrollLock.createSelector((state, triggerId) => triggerId !== undefined && activeTriggerIdSelector(state) === triggerId && state.mounted),
121
- triggerProps: useScrollLock.createSelector((state, isActive) => isActive ? state.activeTriggerProps : state.inactiveTriggerProps),
120
+ isMountedByTrigger: (state, triggerId) => triggerId !== undefined && activeTriggerIdSelector(state) === triggerId && state.mounted,
121
+ triggerProps: (state, isActive) => isActive ? state.activeTriggerProps : state.inactiveTriggerProps,
122
122
  /**
123
123
  * Popup id for the trigger that currently owns the open popup.
124
124
  */
125
- triggerPopupId: useScrollLock.createSelector((state, triggerId) => triggerOwnsOpenPopupOrIsOnlyTrigger(state, triggerId) ? popupIdSelector(state) : undefined),
126
- popupProps: useScrollLock.createSelector(state => state.popupProps),
127
- popupElement: useScrollLock.createSelector(state => state.popupElement),
128
- positionerElement: useScrollLock.createSelector(state => state.positionerElement)
125
+ triggerPopupId: (state, triggerId) => triggerOwnsOpenPopupOrIsOnlyTrigger(state, triggerId) ? popupIdSelector(state) : undefined,
126
+ popupProps: state => state.popupProps,
127
+ popupElement: state => state.popupElement,
128
+ positionerElement: state => state.positionerElement
129
129
  };
130
130
 
131
- const IsDrawerContext = /*#__PURE__*/React__namespace.createContext(false);
132
- if (process.env.NODE_ENV !== "production") IsDrawerContext.displayName = "IsDrawerContext";
131
+ /**
132
+ * Store members a detached handle-backed trigger reads or invokes for trigger registration and data
133
+ * forwarding. `set`/`update` are included only for trigger-count and trigger-data bookkeeping; on a
134
+ * detached (inert) store they are intentionally no-ops, so a write through them is not guaranteed to
135
+ * be durable. Component handle-store views Pick these from their concrete store (preserving its
136
+ * context and selectors) and add any component-specific trigger-invoked members such as `setOpen`.
137
+ */
138
+
139
+ /**
140
+ * The subset of a popup store that trigger registration and data forwarding rely on. Narrow enough
141
+ * that an inert store can be passed while detached.
142
+ */
143
+
144
+ /**
145
+ * Reads the store currently exposed by a popup handle and subscribes to store-pointer changes.
146
+ * Detached triggers use this to follow a handle as a root attaches or detaches: while no root is
147
+ * attached, the handle exposes its fallback store; once a root attaches, subscribers re-render and
148
+ * read from the live root store.
149
+ *
150
+ * Returns `undefined` when no handle is provided so callers can fall back to their root context.
151
+ *
152
+ * @param handle The popup handle to read from, or `undefined` when the trigger is not handle-bound.
153
+ */
154
+ function usePopupHandleStore(handle) {
155
+ const subscribe = React__namespace.useCallback(listener => {
156
+ if (handle === undefined) {
157
+ return useRenderElement.NOOP;
158
+ }
159
+ return handle.subscribeStore(listener);
160
+ }, [handle]);
161
+ const getSnapshot = React__namespace.useCallback(() => {
162
+ return handle === undefined ? undefined : handle.store;
163
+ }, [handle]);
164
+ return useScrollLock.shimExports.useSyncExternalStore(subscribe, getSnapshot, () => handle?.serverStore);
165
+ }
166
+
133
167
  const DialogRootContext = /*#__PURE__*/React__namespace.createContext(undefined);
134
168
  if (process.env.NODE_ENV !== "production") DialogRootContext.displayName = "DialogRootContext";
135
169
  function useDialogRootContext(optional) {
136
- const dialogRootContext = React__namespace.useContext(DialogRootContext);
137
- if (optional === false && dialogRootContext === undefined) {
170
+ const store = React__namespace.useContext(DialogRootContext);
171
+ if (!optional && store === undefined) {
138
172
  throw new Error(process.env.NODE_ENV !== "production" ? 'Base UI: DialogRootContext is missing. Dialog parts must be placed within <Dialog.Root>.' : useRenderElement.formatErrorMessage(27));
139
173
  }
140
- return dialogRootContext;
174
+ return store;
141
175
  }
142
176
 
143
177
  const DialogPortalContext = /*#__PURE__*/React__namespace.createContext(undefined);
@@ -150,14 +184,26 @@ function useDialogPortalContext() {
150
184
  return value;
151
185
  }
152
186
 
187
+ /**
188
+ * Shared by `Dialog.Popup` and `Dialog.Viewport`, whose states have the same shape.
189
+ * `nested` is not mapped: unmapped `true` booleans already render as `data-nested`.
190
+ */
191
+ const dialogStateAttributesMapping = {
192
+ ...useScrollLock.popupStateMapping,
193
+ ...useScrollLock.transitionStatusMapping,
194
+ nestedDialogOpen(value) {
195
+ return value ? {
196
+ 'data-nested-dialog-open': ''
197
+ } : null;
198
+ }
199
+ };
200
+
153
201
  const DialogPortal = /*#__PURE__*/React__namespace.forwardRef(function DialogPortal(props, forwardedRef) {
154
202
  const {
155
203
  keepMounted = false,
156
204
  ...portalProps
157
205
  } = props;
158
- const {
159
- store
160
- } = useDialogRootContext();
206
+ const store = useDialogRootContext();
161
207
  const mounted = store.useState('mounted');
162
208
  const modal = store.useState('modal');
163
209
  const open = store.useState('open');
@@ -179,25 +225,6 @@ const DialogPortal = /*#__PURE__*/React__namespace.forwardRef(function DialogPor
179
225
  });
180
226
  if (process.env.NODE_ENV !== "production") DialogPortal.displayName = "DialogPortal";
181
227
 
182
- function useDialogRoot(params) {
183
- const {
184
- store,
185
- actionsRef
186
- } = params;
187
- const open = store.useState('open');
188
- useScrollLock.usePopupRootSync(store, open);
189
- useScrollLock.useImplicitActiveTrigger(store);
190
- const {
191
- forceUnmount
192
- } = useScrollLock.useOpenStateTransitions(open, store);
193
- const handleImperativeClose = React__namespace.useCallback(() => {
194
- store.setOpen(false, useScrollLock.createChangeEventDetails(useScrollLock.imperativeAction));
195
- }, [store]);
196
- React__namespace.useImperativeHandle(actionsRef, () => ({
197
- unmount: forceUnmount,
198
- close: handleImperativeClose
199
- }), [forceUnmount, handleImperativeClose]);
200
- }
201
228
  function DialogInteractions({
202
229
  store,
203
230
  parentContext,
@@ -233,8 +260,18 @@ function DialogInteractions({
233
260
  if ('button' in event && event.button !== 0) {
234
261
  return false;
235
262
  }
236
- if ('touches' in event && event.touches.length !== 1) {
237
- return false;
263
+ if ('touches' in event) {
264
+ // Outside press can be handled on `touchend`, where the lifted point is
265
+ // reported in `changedTouches` and `touches` contains any remaining
266
+ // active points. Treat it as a single-finger tap only when exactly one
267
+ // touch ended and no other fingers are still down.
268
+ if (event.type === 'touchend') {
269
+ if (event.changedTouches.length !== 1 || event.touches.length !== 0) {
270
+ return false;
271
+ }
272
+ } else if (event.touches.length !== 1) {
273
+ return false;
274
+ }
238
275
  }
239
276
  const target = useScrollLock.getTarget(event);
240
277
  if (isTopmost && !disablePointerDismissal) {
@@ -242,7 +279,9 @@ function DialogInteractions({
242
279
  // This supports multiple modal dialogs that aren't nested in the React tree:
243
280
  // https://github.com/mui/base-ui/issues/1320
244
281
  if (modal) {
245
- return store.context.internalBackdropRef.current || store.context.backdropRef.current ? store.context.internalBackdropRef.current === target || store.context.backdropRef.current === target || useScrollLock.contains(target, popupElement) && !target?.hasAttribute('data-base-ui-portal') : true;
282
+ const internalBackdrop = store.context.internalBackdropRef.current;
283
+ const backdrop = store.context.backdropRef.current;
284
+ return internalBackdrop || backdrop ? internalBackdrop === target || backdrop === target || useScrollLock.contains(target, popupElement) && !target?.hasAttribute('data-base-ui-portal') : true;
246
285
  }
247
286
  return true;
248
287
  }
@@ -253,73 +292,82 @@ function DialogInteractions({
253
292
  useScrollLock.useScrollLock(open && modal === true, popupElement);
254
293
 
255
294
  // Listen for nested open/close events on this store to maintain the counts.
295
+ // A close notification is an open notification with zeroed counts.
256
296
  store.useContextCallback('onNestedDialogOpen', (dialogCount, drawerCount) => {
257
297
  setOwnNestedOpenDialogs(dialogCount);
258
298
  setOwnNestedOpenDrawers(drawerCount);
259
299
  });
260
- store.useContextCallback('onNestedDialogClose', () => {
261
- setOwnNestedOpenDialogs(0);
262
- setOwnNestedOpenDrawers(0);
263
- });
264
300
 
265
301
  // Notify parent of our open/close state using parent callbacks, if any
266
- React__namespace.useEffect(() => {
267
- if (parentContext?.onNestedDialogOpen && open) {
268
- parentContext.onNestedDialogOpen(ownNestedOpenDialogs + 1, ownNestedOpenDrawers + (isDrawer ? 1 : 0));
269
- }
270
- if (parentContext?.onNestedDialogClose && !open) {
271
- parentContext.onNestedDialogClose();
302
+ useScrollLock.useIsoLayoutEffect(() => {
303
+ if (parentContext?.onNestedDialogOpen) {
304
+ if (open) {
305
+ parentContext.onNestedDialogOpen(ownNestedOpenDialogs + 1, ownNestedOpenDrawers + (isDrawer ? 1 : 0));
306
+ } else {
307
+ parentContext.onNestedDialogOpen(0, 0);
308
+ }
272
309
  }
273
310
  return () => {
274
- if (parentContext?.onNestedDialogClose && open) {
275
- parentContext.onNestedDialogClose();
311
+ if (parentContext?.onNestedDialogOpen && open) {
312
+ parentContext.onNestedDialogOpen(0, 0);
276
313
  }
277
314
  };
278
315
  }, [isDrawer, open, ownNestedOpenDialogs, ownNestedOpenDrawers, parentContext]);
279
- const activeTriggerProps = dismiss.reference ?? useRenderElement.EMPTY_OBJECT;
280
- const inactiveTriggerProps = dismiss.trigger ?? useRenderElement.EMPTY_OBJECT;
281
- // Consumers (DialogPopup/DrawerPopup) already spread `FOCUSABLE_POPUP_PROPS`
282
- // directly, so the popup props only need to carry the dismiss handlers.
283
- const popupProps = dismiss.floating ?? useRenderElement.EMPTY_OBJECT;
284
316
  useScrollLock.usePopupInteractionProps(store, {
285
- activeTriggerProps,
286
- inactiveTriggerProps,
287
- popupProps,
317
+ // `enabled` is not passed to `useDismiss`, so its props are always defined,
318
+ // and `trigger` is the same object as `reference`.
319
+ activeTriggerProps: dismiss.reference,
320
+ inactiveTriggerProps: dismiss.trigger,
321
+ // DialogPopup and DrawerPopup spread `FOCUSABLE_POPUP_PROPS` directly, so
322
+ // this only needs to carry the dismiss handlers.
323
+ popupProps: dismiss.floating,
288
324
  nestedOpenDialogCount: ownNestedOpenDialogs,
289
325
  nestedOpenDrawerCount: ownNestedOpenDrawers
290
326
  });
291
327
  return null;
292
328
  }
293
329
 
330
+ /**
331
+ * A `ReactStore` whose state never changes.
332
+ *
333
+ * Useful for fallback stores that need to support normal store reads while detached from the
334
+ * component that owns real state. Context values may still contain mutable refs or maps.
335
+ */
336
+ class NullStore extends useScrollLock.ReactStore {
337
+ // `update`/`set`/`notifyAll` funnel through `setState` in the base `Store`, so overriding
338
+ // `setState` alone would neutralize them today. They are overridden explicitly so the store stays
339
+ // inert even if a future base-class change stops routing a mutator through `setState`.
340
+ setState(_newState) {}
341
+ update(_changes) {}
342
+ set(_key, _value) {}
343
+ notifyAll() {}
344
+ }
345
+
294
346
  const selectors = {
295
347
  ...popupStoreSelectors,
296
- modal: useScrollLock.createSelector(state => state.modal),
297
- nested: useScrollLock.createSelector(state => state.nested),
298
- nestedOpenDialogCount: useScrollLock.createSelector(state => state.nestedOpenDialogCount),
299
- nestedOpenDrawerCount: useScrollLock.createSelector(state => state.nestedOpenDrawerCount),
300
- disablePointerDismissal: useScrollLock.createSelector(state => state.disablePointerDismissal),
301
- openMethod: useScrollLock.createSelector(state => state.openMethod),
302
- descriptionElementId: useScrollLock.createSelector(state => state.descriptionElementId),
303
- titleElementId: useScrollLock.createSelector(state => state.titleElementId),
304
- viewportElement: useScrollLock.createSelector(state => state.viewportElement),
305
- role: useScrollLock.createSelector(state => state.role)
348
+ modal: state => state.modal,
349
+ nested: state => state.nested,
350
+ nestedOpenDialogCount: state => state.nestedOpenDialogCount,
351
+ nestedOpenDrawerCount: state => state.nestedOpenDrawerCount,
352
+ disablePointerDismissal: state => state.disablePointerDismissal,
353
+ openMethod: state => state.openMethod,
354
+ descriptionElementId: state => state.descriptionElementId,
355
+ titleElementId: state => state.titleElementId,
356
+ viewportElement: state => state.viewportElement,
357
+ role: state => state.role
306
358
  };
359
+
360
+ /**
361
+ * The subset of `DialogStore` that detached handle-backed triggers rely on. Both the real
362
+ * `DialogStore` and the inert fallback store satisfy it, so a trigger can read from whichever
363
+ * store the handle currently exposes.
364
+ */
365
+
307
366
  class DialogStore extends useScrollLock.ReactStore {
308
- constructor(initialState, floatingId, nested = false) {
367
+ constructor(initialState, floatingId, nested) {
309
368
  const triggerElements = new useScrollLock.PopupTriggerMap();
310
- const state = createInitialState(initialState);
311
- state.floatingRootContext = createPopupFloatingRootContext(triggerElements, floatingId, nested);
312
- super(state, {
313
- popupRef: /*#__PURE__*/React__namespace.createRef(),
314
- backdropRef: /*#__PURE__*/React__namespace.createRef(),
315
- internalBackdropRef: /*#__PURE__*/React__namespace.createRef(),
316
- outsidePressEnabledRef: {
317
- current: true
318
- },
319
- triggerElements,
320
- onOpenChange: undefined,
321
- onOpenChangeComplete: undefined
322
- }, selectors);
369
+ const state = createInitialState(initialState, triggerElements, floatingId, nested);
370
+ super(state, createInitialContext(triggerElements), selectors);
323
371
  }
324
372
  setOpen = (nextOpen, eventDetails) => {
325
373
  eventDetails.preventUnmountOnClose = () => {
@@ -341,20 +389,22 @@ class DialogStore extends useScrollLock.ReactStore {
341
389
  useScrollLock.setPopupOpenState(updatedState, nextOpen, eventDetails.trigger);
342
390
  this.update(updatedState);
343
391
  };
344
- static useStore(externalStore, initialState) {
345
- /* eslint-disable react-hooks/rules-of-hooks */
346
- const store = useScrollLock.usePopupStore(externalStore, (floatingId, nested) => new DialogStore(initialState, floatingId, nested), true).store;
347
- /* eslint-enable react-hooks/rules-of-hooks */
392
+ }
348
393
 
349
- return store;
350
- }
394
+ /**
395
+ * Creates the inert fallback store used by detached handle-backed triggers while no
396
+ * `Dialog.Root` is attached. It preserves a dialog-specific trigger registry in context so
397
+ * detached triggers can register before migrating to the live root store.
398
+ */
399
+ function createNullDialogStore() {
400
+ const triggerElements = new useScrollLock.PopupTriggerMap();
401
+ return new NullStore(Object.freeze(createInitialState(undefined, triggerElements)), Object.freeze(createInitialContext(triggerElements)), selectors);
351
402
  }
352
- function createInitialState(initialState = {}) {
353
- return {
403
+ function createInitialState(initialState, triggerElements, floatingId, nested = false) {
404
+ const state = {
354
405
  ...createInitialPopupStoreState(),
355
406
  modal: true,
356
407
  disablePointerDismissal: false,
357
- popupElement: null,
358
408
  viewportElement: null,
359
409
  descriptionElementId: undefined,
360
410
  titleElementId: undefined,
@@ -365,9 +415,24 @@ function createInitialState(initialState = {}) {
365
415
  role: 'dialog',
366
416
  ...initialState
367
417
  };
418
+ state.floatingRootContext = createPopupFloatingRootContext(triggerElements, floatingId, nested);
419
+ return state;
420
+ }
421
+ function createInitialContext(triggerElements) {
422
+ return {
423
+ popupRef: /*#__PURE__*/React__namespace.createRef(),
424
+ backdropRef: /*#__PURE__*/React__namespace.createRef(),
425
+ internalBackdropRef: /*#__PURE__*/React__namespace.createRef(),
426
+ outsidePressEnabledRef: {
427
+ current: true
428
+ },
429
+ triggerElements,
430
+ onOpenChange: undefined,
431
+ onOpenChangeComplete: undefined
432
+ };
368
433
  }
369
434
 
370
- function useRenderDialogRoot(props, mode = 'dialog') {
435
+ function useRenderDialogRoot(mode, props) {
371
436
  const {
372
437
  children,
373
438
  open: openProp,
@@ -386,38 +451,26 @@ function useRenderDialogRoot(props, mode = 'dialog') {
386
451
  const modal = isAlertDialog ? true : modalProp;
387
452
  const disablePointerDismissal = isAlertDialog || disablePointerDismissalProp;
388
453
  const role = isAlertDialog ? 'alertdialog' : 'dialog';
389
- const parentDialogRootContext = useDialogRootContext(true);
390
- const nested = Boolean(parentDialogRootContext);
454
+ const parentStore = useDialogRootContext(true);
455
+ const nested = parentStore != null;
391
456
  const rootState = {
392
457
  modal,
393
458
  disablePointerDismissal,
394
459
  nested,
395
460
  role
396
461
  };
397
- const store = DialogStore.useStore(handle?.store, {
462
+
463
+ // The store is owned by this Root instance and created exactly once. It is not tied to the handle:
464
+ // the handle attaches to it, so swapping the handle re-attaches rather than recreating state.
465
+ // Default values are only initial values; controlled values and root state are synced after creation.
466
+ // Dialogs pass the popup element to Floating UI as the floating element (`treatPopupAsFloatingElement`).
467
+ const store = useScrollLock.usePopupRootStore((floatingId, floatingNested) => new DialogStore({
398
468
  open: defaultOpen,
399
469
  openProp,
400
470
  activeTriggerId: defaultTriggerIdProp,
401
471
  triggerIdProp,
402
472
  ...rootState
403
- });
404
-
405
- // Support initially open state when uncontrolled
406
- useScrollLock.useOnFirstRender(() => {
407
- const nextState = openProp === undefined && store.state.open === false && defaultOpen === true ? {
408
- open: true,
409
- activeTriggerId: defaultTriggerIdProp
410
- } : null;
411
- if (isAlertDialog) {
412
- // Handles can reuse plain Dialog stores; alert dialog invariants must exist immediately.
413
- store.update(nextState ? {
414
- ...rootState,
415
- ...nextState
416
- } : rootState);
417
- } else if (nextState) {
418
- store.update(nextState);
419
- }
420
- });
473
+ }, floatingId, floatingNested), true);
421
474
  store.useControlledProp('openProp', openProp);
422
475
  store.useControlledProp('triggerIdProp', triggerIdProp);
423
476
  store.useSyncedValues(rootState);
@@ -426,83 +479,31 @@ function useRenderDialogRoot(props, mode = 'dialog') {
426
479
  const open = store.useState('open');
427
480
  const mounted = store.useState('mounted');
428
481
  const payload = store.useState('payload');
429
- useDialogRoot({
430
- store,
431
- actionsRef
432
- });
482
+ useScrollLock.usePopupRootSync(store, open);
483
+ useScrollLock.useImplicitActiveTrigger(store);
484
+ const {
485
+ forceUnmount
486
+ } = useScrollLock.useOpenStateTransitions(open, store);
487
+ React__namespace.useImperativeHandle(actionsRef, () => ({
488
+ unmount: forceUnmount,
489
+ close: () => store.setOpen(false, useScrollLock.createChangeEventDetails(useScrollLock.imperativeAction))
490
+ }), [forceUnmount, store]);
433
491
  const shouldRenderInteractions = open || mounted;
434
- const contextValue = React__namespace.useMemo(() => ({
435
- store
436
- }), [store]);
437
- return /*#__PURE__*/jsxRuntime.jsx(IsDrawerContext.Provider, {
438
- value: false,
439
- children: /*#__PURE__*/jsxRuntime.jsxs(DialogRootContext.Provider, {
440
- value: contextValue,
441
- children: [shouldRenderInteractions && /*#__PURE__*/jsxRuntime.jsx(DialogInteractions, {
442
- store: store,
443
- parentContext: parentDialogRootContext?.store.context,
444
- isDrawer: isDrawer
445
- }), typeof children === 'function' ? children({
446
- payload
447
- }) : children]
448
- })
492
+ return /*#__PURE__*/jsxRuntime.jsxs(DialogRootContext.Provider, {
493
+ value: store,
494
+ children: [handle && /*#__PURE__*/jsxRuntime.jsx(useScrollLock.PopupHandleAttachment, {
495
+ handle: handle,
496
+ store: store
497
+ }), shouldRenderInteractions && /*#__PURE__*/jsxRuntime.jsx(DialogInteractions, {
498
+ store: store,
499
+ parentContext: parentStore?.context,
500
+ isDrawer: isDrawer
501
+ }), typeof children === 'function' ? children({
502
+ payload
503
+ }) : children]
449
504
  });
450
505
  }
451
506
 
452
- /**
453
- * Groups all parts of the dialog.
454
- * Doesn't render its own HTML element.
455
- *
456
- * Documentation: [Base UI Dialog](https://base-ui.com/react/components/dialog)
457
- */
458
- function DialogRoot(props) {
459
- const mode = React__namespace.useContext(IsDrawerContext) ? 'drawer' : 'dialog';
460
- return useRenderDialogRoot(props, mode);
461
- }
462
-
463
- let DialogViewportDataAttributes = function (DialogViewportDataAttributes) {
464
- /**
465
- * Present when the dialog is open.
466
- */
467
- DialogViewportDataAttributes[DialogViewportDataAttributes["open"] = useScrollLock.CommonPopupDataAttributes.open] = "open";
468
- /**
469
- * Present when the dialog is closed.
470
- */
471
- DialogViewportDataAttributes[DialogViewportDataAttributes["closed"] = useScrollLock.CommonPopupDataAttributes.closed] = "closed";
472
- /**
473
- * Present when the dialog is animating in.
474
- */
475
- DialogViewportDataAttributes[DialogViewportDataAttributes["startingStyle"] = useScrollLock.CommonPopupDataAttributes.startingStyle] = "startingStyle";
476
- /**
477
- * Present when the dialog is animating out.
478
- */
479
- DialogViewportDataAttributes[DialogViewportDataAttributes["endingStyle"] = useScrollLock.CommonPopupDataAttributes.endingStyle] = "endingStyle";
480
- /**
481
- * Present when the dialog is nested within another dialog.
482
- */
483
- DialogViewportDataAttributes["nested"] = "data-nested";
484
- /**
485
- * Present when the dialog has other open dialogs nested within it.
486
- */
487
- DialogViewportDataAttributes["nestedDialogOpen"] = "data-nested-dialog-open";
488
- return DialogViewportDataAttributes;
489
- }({});
490
-
491
- const stateAttributesMapping$2 = {
492
- ...useScrollLock.popupStateMapping,
493
- ...useScrollLock.transitionStatusMapping,
494
- nested(value) {
495
- return value ? {
496
- [DialogViewportDataAttributes.nested]: ''
497
- } : null;
498
- },
499
- nestedDialogOpen(value) {
500
- return value ? {
501
- [DialogViewportDataAttributes.nestedDialogOpen]: ''
502
- } : null;
503
- }
504
- };
505
-
506
507
  /**
507
508
  * A positioning container for the dialog popup that can be made scrollable.
508
509
  * Renders a `<div>` element.
@@ -518,9 +519,7 @@ const DialogViewport = /*#__PURE__*/React__namespace.forwardRef(function DialogV
518
519
  ...elementProps
519
520
  } = componentProps;
520
521
  const keepMounted = useDialogPortalContext();
521
- const {
522
- store
523
- } = useDialogRootContext();
522
+ const store = useDialogRootContext();
524
523
  const open = store.useState('open');
525
524
  const nested = store.useState('nested');
526
525
  const transitionStatus = store.useState('transitionStatus');
@@ -539,7 +538,7 @@ const DialogViewport = /*#__PURE__*/React__namespace.forwardRef(function DialogV
539
538
  enabled: shouldRender,
540
539
  state,
541
540
  ref: [forwardedRef, setViewportElement],
542
- stateAttributesMapping: stateAttributesMapping$2,
541
+ stateAttributesMapping: dialogStateAttributesMapping,
543
542
  props: [{
544
543
  role: 'presentation',
545
544
  hidden: !mounted,
@@ -570,8 +569,9 @@ const DialogTrigger = /*#__PURE__*/React__namespace.forwardRef(function DialogTr
570
569
  handle,
571
570
  ...elementProps
572
571
  } = componentProps;
573
- const dialogRootContext = useDialogRootContext(true);
574
- const store = handle?.store ?? dialogRootContext?.store;
572
+ const dialogRootStore = useDialogRootContext(true);
573
+ const handleStore = usePopupHandleStore(handle);
574
+ const store = handleStore ?? dialogRootStore;
575
575
  if (!store) {
576
576
  throw new Error(process.env.NODE_ENV !== "production" ? 'Base UI: <Dialog.Trigger> must be used within <Dialog.Root> or provided with a handle.' : useRenderElement.formatErrorMessage(79));
577
577
  }
@@ -593,9 +593,7 @@ const DialogTrigger = /*#__PURE__*/React__namespace.forwardRef(function DialogTr
593
593
  disabled,
594
594
  native: nativeButton
595
595
  });
596
- const click = useScrollLock.useClick(floatingContext, {
597
- enabled: floatingContext != null
598
- });
596
+ const click = useScrollLock.useClick(floatingContext);
599
597
  const interactionTypeProps = useScrollLock.useOpenMethodTriggerProps(() => store.select('open'), interactionType => {
600
598
  store.set('openMethod', interactionType);
601
599
  });
@@ -667,11 +665,6 @@ let DrawerBackdropCssVars = /*#__PURE__*/function (DrawerBackdropCssVars) {
667
665
  return DrawerBackdropCssVars;
668
666
  }({});
669
667
 
670
- const stateAttributesMapping$1 = {
671
- ...useScrollLock.popupStateMapping,
672
- ...useScrollLock.transitionStatusMapping
673
- };
674
-
675
668
  /**
676
669
  * An overlay displayed beneath the popup.
677
670
  * Renders a `<div>` element.
@@ -686,9 +679,7 @@ const DrawerBackdrop = /*#__PURE__*/React__namespace.forwardRef(function DrawerB
686
679
  forceRender = false,
687
680
  ...elementProps
688
681
  } = componentProps;
689
- const {
690
- store
691
- } = useDialogRootContext();
682
+ const store = useDialogRootContext();
692
683
  const open = store.useState('open');
693
684
  const nested = store.useState('nested');
694
685
  const mounted = store.useState('mounted');
@@ -700,7 +691,7 @@ const DrawerBackdrop = /*#__PURE__*/React__namespace.forwardRef(function DrawerB
700
691
  return useRenderElement.useRenderElement('div', componentProps, {
701
692
  state,
702
693
  ref: [store.context.backdropRef, forwardedRef],
703
- stateAttributesMapping: stateAttributesMapping$1,
694
+ stateAttributesMapping: useScrollLock.popupTransitionStateMapping,
704
695
  props: [{
705
696
  role: 'presentation',
706
697
  hidden: !mounted,
@@ -744,9 +735,8 @@ if (process.env.NODE_ENV !== "production") DrawerContent.displayName = "DrawerCo
744
735
 
745
736
  const DrawerProviderContext = /*#__PURE__*/React__namespace.createContext(undefined);
746
737
  if (process.env.NODE_ENV !== "production") DrawerProviderContext.displayName = "DrawerProviderContext";
747
- function useDrawerProviderContext(optional) {
748
- const context = React__namespace.useContext(DrawerProviderContext);
749
- return context;
738
+ function useDrawerProviderContext() {
739
+ return React__namespace.useContext(DrawerProviderContext);
750
740
  }
751
741
 
752
742
  let DrawerPopupDataAttributes = function (DrawerPopupDataAttributes) {
@@ -759,7 +749,7 @@ let DrawerPopupDataAttributes = function (DrawerPopupDataAttributes) {
759
749
  */
760
750
  DrawerPopupDataAttributes[DrawerPopupDataAttributes["closed"] = useScrollLock.CommonPopupDataAttributes.closed] = "closed";
761
751
  /**
762
- * Present when the drawer is animating in.
752
+ * Present when the drawer begins animating in.
763
753
  */
764
754
  DrawerPopupDataAttributes[DrawerPopupDataAttributes["startingStyle"] = useScrollLock.CommonPopupDataAttributes.startingStyle] = "startingStyle";
765
755
  /**
@@ -798,7 +788,7 @@ const DrawerRootContext = /*#__PURE__*/React__namespace.createContext(undefined)
798
788
  if (process.env.NODE_ENV !== "production") DrawerRootContext.displayName = "DrawerRootContext";
799
789
  function useDrawerRootContext(optional) {
800
790
  const drawerRootContext = React__namespace.useContext(DrawerRootContext);
801
- if (optional === false && drawerRootContext === undefined) {
791
+ if (optional !== true && drawerRootContext === undefined) {
802
792
  throw new Error(process.env.NODE_ENV !== "production" ? 'Base UI: DrawerRootContext is missing. Drawer parts must be placed within <Drawer.Root>.' : useRenderElement.formatErrorMessage(90));
803
793
  }
804
794
  return drawerRootContext;
@@ -839,22 +829,24 @@ function resolveSnapPointValue(snapPoint, viewportHeight, rootFontSize) {
839
829
  }
840
830
  return null;
841
831
  }
842
- function findClosestSnapPoint(height, points) {
843
- let closest = null;
832
+
833
+ /**
834
+ * Returns the index of the value closest to `target`, or `-1` if `values` is empty.
835
+ */
836
+ function closestSnapPointIndex(values, target) {
837
+ let closestIndex = -1;
844
838
  let closestDistance = Infinity;
845
- for (const point of points) {
846
- const distance = Math.abs(point.height - height);
839
+ for (let index = 0; index < values.length; index += 1) {
840
+ const distance = Math.abs(values[index] - target);
847
841
  if (distance < closestDistance) {
848
842
  closestDistance = distance;
849
- closest = point;
843
+ closestIndex = index;
850
844
  }
851
845
  }
852
- return closest;
846
+ return closestIndex;
853
847
  }
854
848
  function useDrawerSnapPoints() {
855
- const {
856
- store
857
- } = useDialogRootContext();
849
+ const store = useDialogRootContext();
858
850
  const {
859
851
  snapPoints,
860
852
  activeSnapPoint,
@@ -867,12 +859,7 @@ function useDrawerSnapPoints() {
867
859
  const measureViewportHeight = useScrollLock.useStableCallback(() => {
868
860
  const doc = useScrollLock.ownerDocument(viewportElement);
869
861
  const html = doc.documentElement;
870
- if (viewportElement) {
871
- setViewportHeight(viewportElement.offsetHeight);
872
- }
873
- if (!viewportElement) {
874
- setViewportHeight(html.clientHeight);
875
- }
862
+ setViewportHeight(viewportElement ? viewportElement.offsetHeight : html.clientHeight);
876
863
  const fontSize = parseFloat(getComputedStyle(html).fontSize);
877
864
  if (Number.isFinite(fontSize)) {
878
865
  setRootFontSize(fontSize);
@@ -894,12 +881,9 @@ function useDrawerSnapPoints() {
894
881
  return [];
895
882
  }
896
883
  const maxHeight = Math.min(popupHeight, viewportHeight);
897
- if (!Number.isFinite(maxHeight) || maxHeight <= 0) {
898
- return [];
899
- }
900
884
  const resolved = snapPoints.map(value => {
901
885
  const resolvedHeight = resolveSnapPointValue(value, viewportHeight, rootFontSize);
902
- if (resolvedHeight === null || !Number.isFinite(resolvedHeight)) {
886
+ if (resolvedHeight === null) {
903
887
  return null;
904
888
  }
905
889
  const clampedHeight = scrollEdges.clamp(resolvedHeight, 0, maxHeight);
@@ -927,9 +911,6 @@ function useDrawerSnapPoints() {
927
911
  return deduped;
928
912
  }, [popupHeight, rootFontSize, snapPoints, viewportHeight]);
929
913
  const resolvedActiveSnapPoint = React__namespace.useMemo(() => {
930
- if (activeSnapPoint === undefined) {
931
- return resolvedSnapPoints[0];
932
- }
933
914
  if (activeSnapPoint === null) {
934
915
  return undefined;
935
916
  }
@@ -939,11 +920,11 @@ function useDrawerSnapPoints() {
939
920
  }
940
921
  const maxHeight = Math.min(popupHeight, viewportHeight);
941
922
  const resolvedHeight = resolveSnapPointValue(activeSnapPoint, viewportHeight, rootFontSize);
942
- if (resolvedHeight === null || !Number.isFinite(resolvedHeight)) {
923
+ if (resolvedHeight === null) {
943
924
  return undefined;
944
925
  }
945
926
  const clampedHeight = scrollEdges.clamp(resolvedHeight, 0, maxHeight);
946
- return findClosestSnapPoint(clampedHeight, resolvedSnapPoints) ?? undefined;
927
+ return resolvedSnapPoints[closestSnapPointIndex(resolvedSnapPoints.map(point => point.height), clampedHeight)];
947
928
  }, [activeSnapPoint, popupHeight, resolvedSnapPoints, rootFontSize, viewportHeight]);
948
929
  return {
949
930
  snapPoints,
@@ -958,9 +939,8 @@ function useDrawerSnapPoints() {
958
939
 
959
940
  const DrawerViewportContext = /*#__PURE__*/React__namespace.createContext(null);
960
941
  if (process.env.NODE_ENV !== "production") DrawerViewportContext.displayName = "DrawerViewportContext";
961
- function useDrawerViewportContext(optional) {
962
- const context = React__namespace.useContext(DrawerViewportContext);
963
- return context;
942
+ function useDrawerViewportContext() {
943
+ return React__namespace.useContext(DrawerViewportContext);
964
944
  }
965
945
 
966
946
  let drawerSwipeVarsRegistered = false;
@@ -1018,8 +998,7 @@ function removeCSSVariableInheritance$1() {
1018
998
  drawerSwipeVarsRegistered = true;
1019
999
  }
1020
1000
  const stateAttributesMapping = {
1021
- ...useScrollLock.popupStateMapping,
1022
- ...useScrollLock.transitionStatusMapping,
1001
+ ...useScrollLock.popupTransitionStateMapping,
1023
1002
  expanded(value) {
1024
1003
  return value ? {
1025
1004
  [DrawerPopupDataAttributes.expanded]: ''
@@ -1036,9 +1015,9 @@ const stateAttributesMapping = {
1036
1015
  } : null;
1037
1016
  },
1038
1017
  swipeDirection(value) {
1039
- return value ? {
1018
+ return {
1040
1019
  [DrawerPopupDataAttributes.swipeDirection]: value
1041
- } : null;
1020
+ };
1042
1021
  },
1043
1022
  swiping(value) {
1044
1023
  return value ? {
@@ -1062,9 +1041,7 @@ const DrawerPopup = /*#__PURE__*/React__namespace.forwardRef(function DrawerPopu
1062
1041
  initialFocus,
1063
1042
  ...elementProps
1064
1043
  } = componentProps;
1065
- const {
1066
- store
1067
- } = useDialogRootContext();
1044
+ const store = useDialogRootContext();
1068
1045
  const popupRef = store.context.popupRef;
1069
1046
  const {
1070
1047
  swipeDirection,
@@ -1103,6 +1080,8 @@ const DrawerPopup = /*#__PURE__*/React__namespace.forwardRef(function DrawerPopu
1103
1080
  const swipeStrength = swipe?.swipeStrength ?? null;
1104
1081
  const [popupHeight, setPopupHeight] = React__namespace.useState(0);
1105
1082
  const popupHeightRef = React__namespace.useRef(0);
1083
+
1084
+ /* istanbul ignore else -- process.env.NODE_ENV is a build-time constant. */
1106
1085
  if (process.env.NODE_ENV !== 'production') {
1107
1086
  // eslint-disable-next-line react-hooks/rules-of-hooks
1108
1087
  React__namespace.useEffect(() => {
@@ -1185,7 +1164,7 @@ const DrawerPopup = /*#__PURE__*/React__namespace.forwardRef(function DrawerPopu
1185
1164
  }
1186
1165
  };
1187
1166
  }, [nestedSwipeProgressStore, popupRef]);
1188
- React__namespace.useEffect(() => {
1167
+ useScrollLock.useIsoLayoutEffect(() => {
1189
1168
  if (!open) {
1190
1169
  return undefined;
1191
1170
  }
@@ -1194,7 +1173,7 @@ const DrawerPopup = /*#__PURE__*/React__namespace.forwardRef(function DrawerPopu
1194
1173
  notifyParentFrontmostHeight?.(0);
1195
1174
  };
1196
1175
  }, [frontmostHeight, open, notifyParentFrontmostHeight]);
1197
- React__namespace.useEffect(() => {
1176
+ useScrollLock.useIsoLayoutEffect(() => {
1198
1177
  if (!notifyParentHasNestedDrawer) {
1199
1178
  return undefined;
1200
1179
  }
@@ -1238,7 +1217,7 @@ const DrawerPopup = /*#__PURE__*/React__namespace.forwardRef(function DrawerPopu
1238
1217
  let dragStyles = swipe ? swipe.getDragStyles() : useRenderElement.EMPTY_OBJECT;
1239
1218
  if (shouldApplySnapPoints && swipeDirection === 'down') {
1240
1219
  const baseOffset = activeSnapPointOffset ?? 0;
1241
- const movementValue = Number.parseFloat(String(dragStyles[DrawerPopupCssVars.swipeMovementY] ?? 0));
1220
+ const movementValue = Number.parseFloat(String(dragStyles[DrawerPopupCssVars.swipeMovementY]));
1242
1221
  if (swiping && Number.isFinite(movementValue)) {
1243
1222
  dragStyles = {
1244
1223
  ...dragStyles,
@@ -1348,6 +1327,7 @@ function DrawerRoot(props) {
1348
1327
  state: 'snapPoint'
1349
1328
  });
1350
1329
  const isNestedDrawerOpenRef = React__namespace.useRef(false);
1330
+ const swipeAreaActiveRef = React__namespace.useRef(false);
1351
1331
  const setActiveSnapPoint = useScrollLock.useStableCallback((nextSnapPoint, eventDetails) => {
1352
1332
  const resolvedEventDetails = eventDetails ?? useScrollLock.createChangeEventDetails(useScrollLock.none);
1353
1333
  onSnapPointChange?.(nextSnapPoint, resolvedEventDetails);
@@ -1407,6 +1387,7 @@ function DrawerRoot(props) {
1407
1387
  });
1408
1388
  const contextValue = React__namespace.useMemo(() => ({
1409
1389
  swipeDirection,
1390
+ swipeAreaActiveRef,
1410
1391
  snapToSequentialPoints,
1411
1392
  snapPoints,
1412
1393
  activeSnapPoint: resolvedActiveSnapPoint,
@@ -1425,30 +1406,28 @@ function DrawerRoot(props) {
1425
1406
  notifyParentSwipingChange,
1426
1407
  notifyParentSwipeProgressChange,
1427
1408
  notifyParentHasNestedDrawer
1428
- }), [resolvedActiveSnapPoint, frontmostHeight, hasNestedDrawer, nestedSwiping, nestedSwipeProgressStore, notifyParentHasNestedDrawer, notifyParentSwipeProgressChange, notifyParentSwipingChange, notifyParentFrontmostHeight, onNestedDrawerPresenceChange, onNestedFrontmostHeightChange, onNestedSwipeProgressChange, onNestedSwipingChange, onPopupHeightChange, popupHeight, setActiveSnapPoint, snapPoints, snapToSequentialPoints, swipeDirection]);
1409
+ }), [resolvedActiveSnapPoint, frontmostHeight, hasNestedDrawer, nestedSwiping, nestedSwipeProgressStore, notifyParentHasNestedDrawer, notifyParentSwipeProgressChange, notifyParentSwipingChange, notifyParentFrontmostHeight, onNestedDrawerPresenceChange, onNestedFrontmostHeightChange, onNestedSwipeProgressChange, onNestedSwipingChange, onPopupHeightChange, popupHeight, setActiveSnapPoint, snapPoints, snapToSequentialPoints, swipeAreaActiveRef, swipeDirection]);
1429
1410
  const resolvedChildren = typeof children === 'function' ? payload => /*#__PURE__*/jsxRuntime.jsxs(React__namespace.Fragment, {
1430
1411
  children: [_DrawerProviderReport || (_DrawerProviderReport = /*#__PURE__*/jsxRuntime.jsx(DrawerProviderReporter, {})), children(payload)]
1431
1412
  }) : /*#__PURE__*/jsxRuntime.jsxs(React__namespace.Fragment, {
1432
1413
  children: [_DrawerProviderReport2 || (_DrawerProviderReport2 = /*#__PURE__*/jsxRuntime.jsx(DrawerProviderReporter, {})), children]
1433
1414
  });
1415
+ const dialog = useRenderDialogRoot('drawer', {
1416
+ open: openProp,
1417
+ defaultOpen,
1418
+ onOpenChange: handleOpenChange,
1419
+ onOpenChangeComplete,
1420
+ disablePointerDismissal,
1421
+ modal,
1422
+ actionsRef,
1423
+ handle,
1424
+ triggerId: triggerIdProp,
1425
+ defaultTriggerId: defaultTriggerIdProp,
1426
+ children: resolvedChildren
1427
+ });
1434
1428
  return /*#__PURE__*/jsxRuntime.jsx(DrawerRootContext.Provider, {
1435
1429
  value: contextValue,
1436
- children: /*#__PURE__*/jsxRuntime.jsx(IsDrawerContext.Provider, {
1437
- value: true,
1438
- children: /*#__PURE__*/jsxRuntime.jsx(DialogRoot, {
1439
- open: openProp,
1440
- defaultOpen: defaultOpen,
1441
- onOpenChange: handleOpenChange,
1442
- onOpenChangeComplete: onOpenChangeComplete,
1443
- disablePointerDismissal: disablePointerDismissal,
1444
- modal: modal,
1445
- actionsRef: actionsRef,
1446
- handle: handle,
1447
- triggerId: triggerIdProp,
1448
- defaultTriggerId: defaultTriggerIdProp,
1449
- children: resolvedChildren
1450
- })
1451
- })
1430
+ children: dialog
1452
1431
  });
1453
1432
  }
1454
1433
  function createNestedSwipeProgressStore() {
@@ -1475,36 +1454,32 @@ function createNestedSwipeProgressStore() {
1475
1454
  };
1476
1455
  }
1477
1456
  function DrawerProviderReporter() {
1478
- const drawerId = useScrollLock.useId();
1479
1457
  const providerContext = useDrawerProviderContext();
1480
- const {
1481
- store
1482
- } = useDialogRootContext(false);
1458
+ const store = useDialogRootContext(false);
1459
+ const setDrawerOpen = providerContext?.setDrawerOpen;
1460
+ const removeDrawer = providerContext?.removeDrawer;
1483
1461
  const open = store.useState('open');
1484
1462
  const nestedOpenDialogCount = store.useState('nestedOpenDialogCount');
1485
1463
  const popupElement = store.useState('popupElement');
1486
1464
  const isTopmost = nestedOpenDialogCount === 0;
1487
- React__namespace.useEffect(() => {
1488
- if (!providerContext || drawerId == null) {
1465
+ useScrollLock.useIsoLayoutEffect(() => {
1466
+ if (!removeDrawer) {
1489
1467
  return undefined;
1490
1468
  }
1491
1469
  return () => {
1492
- providerContext.removeDrawer(drawerId);
1470
+ removeDrawer(store);
1493
1471
  };
1494
- }, [drawerId, providerContext]);
1495
- React__namespace.useEffect(() => {
1496
- if (drawerId == null) {
1497
- return;
1498
- }
1499
- providerContext?.setDrawerOpen(drawerId, open);
1500
- }, [drawerId, open, providerContext]);
1472
+ }, [removeDrawer, store]);
1473
+ useScrollLock.useIsoLayoutEffect(() => {
1474
+ setDrawerOpen?.(store, open);
1475
+ }, [open, setDrawerOpen, store]);
1501
1476
  React__namespace.useEffect(() => {
1502
1477
  // CloseWatcher enables the Android back gesture (Chromium-only).
1503
1478
  // Keep this Android-only for now to avoid interfering with Escape/nesting semantics on desktop due to `useDismiss`.
1504
1479
  if (!open || !isTopmost || !useScrollLock.android) {
1505
1480
  return undefined;
1506
1481
  }
1507
- const win = floatingUi_utils.getWindow(popupElement);
1482
+ const win = useScrollLock.getWindow(popupElement);
1508
1483
  const CloseWatcherCtor = win.CloseWatcher;
1509
1484
  if (!CloseWatcherCtor) {
1510
1485
  return undefined;
@@ -1525,53 +1500,11 @@ function DrawerProviderReporter() {
1525
1500
  return null;
1526
1501
  }
1527
1502
 
1528
- function isScrollable(element, axis,
1529
- // When true, a container that overflows only once extra space is added (e.g. drawer
1530
- // keyboard scroll slack) still counts, as long as it has layout size on the axis.
1531
- allowOverflowIntent = false) {
1532
- const style = floatingUi_utils.getComputedStyle(element);
1533
- if (axis === 'vertical') {
1534
- const overflowY = style.overflowY;
1535
- if (overflowY !== 'auto' && overflowY !== 'scroll') {
1536
- return false;
1537
- }
1538
- return allowOverflowIntent ? element.clientHeight > 0 : element.scrollHeight > element.clientHeight;
1539
- }
1540
- const overflowX = style.overflowX;
1541
- if (overflowX !== 'auto' && overflowX !== 'scroll') {
1542
- return false;
1543
- }
1544
- return allowOverflowIntent ? element.clientWidth > 0 : element.scrollWidth > element.clientWidth;
1545
- }
1546
- function hasScrollableAncestor(target, root, axes) {
1547
- // `getParentNode` crosses shadow boundaries (and slots), so a target inside a shadow root
1548
- // still walks up to scrollable ancestors in the light DOM.
1549
- let node = target;
1550
- while (floatingUi_utils.isHTMLElement(node) && node !== root && !floatingUi_utils.isLastTraversableNode(node)) {
1551
- for (const axis of axes) {
1552
- if (isScrollable(node, axis)) {
1553
- return true;
1554
- }
1555
- }
1556
- node = floatingUi_utils.getParentNode(node);
1557
- }
1558
- return false;
1559
- }
1560
- function findScrollableTouchTarget(target, root, axis = 'vertical', allowOverflowIntent = false) {
1561
- // `getParentNode` crosses shadow boundaries (and slots), so a target inside a shadow root
1562
- // still reaches a scrollable ancestor in the light DOM.
1563
- let node = floatingUi_utils.isHTMLElement(target) ? target : null;
1564
- while (floatingUi_utils.isHTMLElement(node) && node !== root && !floatingUi_utils.isLastTraversableNode(node)) {
1565
- if (isScrollable(node, axis, allowOverflowIntent)) {
1566
- return node;
1567
- }
1568
- node = floatingUi_utils.getParentNode(node);
1569
- }
1570
- return isScrollable(root, axis, allowOverflowIntent) ? root : null;
1571
- }
1572
-
1573
- function getElementAtPoint(doc, x, y) {
1574
- return typeof doc?.elementFromPoint === 'function' ? doc.elementFromPoint(x, y) : null;
1503
+ // `Document.elementFromPoint` retargets shadow content to the shadow host, which then fails
1504
+ // `contains()` checks against a popup inside that shadow root, so callers pass `getRootNode()`
1505
+ // (a document or a shadow root) rather than `ownerDocument()`.
1506
+ function getElementAtPoint(root, x, y) {
1507
+ return typeof root?.elementFromPoint === 'function' ? root.elementFromPoint(x, y) : null;
1575
1508
  }
1576
1509
 
1577
1510
  const DEFAULT_SWIPE_THRESHOLD = 40;
@@ -1596,7 +1529,7 @@ function getDisplacement(direction, deltaX, deltaY) {
1596
1529
  }
1597
1530
  }
1598
1531
  function getElementTransform(element) {
1599
- const computedStyle = floatingUi_utils.getWindow(element).getComputedStyle(element);
1532
+ const computedStyle = useScrollLock.getWindow(element).getComputedStyle(element);
1600
1533
  const transform = computedStyle.transform;
1601
1534
  let translateX = 0;
1602
1535
  let translateY = 0;
@@ -1718,6 +1651,7 @@ function useSwipeDismiss(options) {
1718
1651
  });
1719
1652
  const swipeProgressRef = React__namespace.useRef(0);
1720
1653
  const swipeThresholdRef = React__namespace.useRef(swipeThresholdDefault);
1654
+ const swipeThresholdFunctionRef = React__namespace.useRef(null);
1721
1655
  const swipeStartTimeRef = React__namespace.useRef(null);
1722
1656
  const lastDragSampleRef = React__namespace.useRef(null);
1723
1657
  const lastDragVelocityRef = React__namespace.useRef({
@@ -1739,15 +1673,12 @@ function useSwipeDismiss(options) {
1739
1673
  if (!direction) {
1740
1674
  return;
1741
1675
  }
1742
- if (typeof swipeThresholdProp !== 'function') {
1743
- swipeThresholdRef.current = swipeThresholdDefault;
1744
- return;
1745
- }
1746
1676
  const element = elementRef.current;
1747
- if (!element) {
1677
+ const thresholdFunction = swipeThresholdFunctionRef.current;
1678
+ if (!element || !thresholdFunction) {
1748
1679
  return;
1749
1680
  }
1750
- const value = swipeThresholdProp({
1681
+ const value = thresholdFunction({
1751
1682
  element,
1752
1683
  direction
1753
1684
  });
@@ -1825,6 +1756,7 @@ function useSwipeDismiss(options) {
1825
1756
  setDragDismissed(false);
1826
1757
  updateSwipeProgress(0);
1827
1758
  swipeThresholdRef.current = swipeThresholdDefault;
1759
+ swipeThresholdFunctionRef.current = null;
1828
1760
  dragStartPosRef.current = {
1829
1761
  x: 0,
1830
1762
  y: 0
@@ -1865,11 +1797,6 @@ function useSwipeDismiss(options) {
1865
1797
  lastProgressDetailsRef.current = null;
1866
1798
  syncDragStyles(false);
1867
1799
  }, [setSwiping, swipeThresholdDefault, syncDragStyles, updateSwipeProgress]);
1868
- React__namespace.useEffect(() => {
1869
- if (typeof swipeThresholdProp !== 'function') {
1870
- swipeThresholdRef.current = swipeThresholdDefault;
1871
- }
1872
- }, [swipeThresholdDefault, swipeThresholdProp]);
1873
1800
  function getPrimaryPointerPosition(event) {
1874
1801
  if ('touches' in event) {
1875
1802
  const touch = event.touches[0];
@@ -1890,19 +1817,19 @@ function useSwipeDismiss(options) {
1890
1817
  return event.pointerType === 'touch';
1891
1818
  }
1892
1819
  function getTargetAtPoint(position, nativeEvent) {
1893
- const doc = useScrollLock.ownerDocument(elementRef.current);
1894
- const elementAtPoint = getElementAtPoint(doc, position.x, position.y);
1820
+ const root = elementRef.current?.getRootNode();
1821
+ const elementAtPoint = getElementAtPoint(root, position.x, position.y);
1895
1822
  const target = elementAtPoint ?? useScrollLock.getTarget(nativeEvent);
1896
1823
  return target;
1897
1824
  }
1898
1825
  function findGestureScrollableTouchTarget(target, root) {
1899
1826
  if (hasHorizontal && !hasVertical) {
1900
- return findScrollableTouchTarget(target, root, 'horizontal');
1827
+ return scrollable.findScrollableTouchTarget(target, root, 'horizontal');
1901
1828
  }
1902
1829
  if (hasVertical && !hasHorizontal) {
1903
- return findScrollableTouchTarget(target, root, 'vertical');
1830
+ return scrollable.findScrollableTouchTarget(target, root, 'vertical');
1904
1831
  }
1905
- return findScrollableTouchTarget(target, root, 'vertical') ?? findScrollableTouchTarget(target, root, 'horizontal');
1832
+ return scrollable.findScrollableTouchTarget(target, root, 'vertical') ?? scrollable.findScrollableTouchTarget(target, root, 'horizontal');
1906
1833
  }
1907
1834
  function startSwipeAtPosition(event, position, startOptions) {
1908
1835
  swipeFromScrollableRef.current = false;
@@ -1923,7 +1850,7 @@ function useSwipeDismiss(options) {
1923
1850
  const element = elementRef.current;
1924
1851
  if (ignoreScrollableAncestors && element && target && scrollAxes.length > 0) {
1925
1852
  const ignoreAncestors = startOptions?.ignoreScrollableAncestors ?? false;
1926
- if (!ignoreAncestors && hasScrollableAncestor(target, element, scrollAxes)) {
1853
+ if (!ignoreAncestors && scrollable.hasScrollableAncestor(target, element, scrollAxes)) {
1927
1854
  return false;
1928
1855
  }
1929
1856
  }
@@ -1934,6 +1861,8 @@ function useSwipeDismiss(options) {
1934
1861
  swipeStartTimeRef.current = getValidTimeStamp(event.timeStamp);
1935
1862
  swipeCancelBaselineRef.current = position;
1936
1863
  lastMovePosRef.current = position;
1864
+ swipeThresholdRef.current = swipeThresholdDefault;
1865
+ swipeThresholdFunctionRef.current = typeof swipeThresholdProp === 'function' ? swipeThresholdProp : null;
1937
1866
  if (element) {
1938
1867
  elementSizeRef.current = {
1939
1868
  width: element.offsetWidth,
@@ -1998,12 +1927,9 @@ function useSwipeDismiss(options) {
1998
1927
  onCancel?.(event.nativeEvent);
1999
1928
  }
2000
1929
  function applyDirectionalDamping(deltaX, deltaY) {
2001
- const exponent = value => value >= 0 ? value ** 0.5 : -(Math.abs(value) ** 0.5);
1930
+ const exponent = value => Math.sign(value) * Math.abs(value) ** 0.5;
2002
1931
  const dampAxis = (delta, allowNegative, allowPositive) => {
2003
- if (!allowNegative && delta < 0) {
2004
- return exponent(delta);
2005
- }
2006
- if (!allowPositive && delta > 0) {
1932
+ if (!allowNegative && delta < 0 || !allowPositive && delta > 0) {
2007
1933
  return exponent(delta);
2008
1934
  }
2009
1935
  return delta;
@@ -2016,29 +1942,16 @@ function useSwipeDismiss(options) {
2016
1942
  };
2017
1943
  }
2018
1944
  function canSwipeFromScrollEdgeOnPendingMove(scrollTarget, deltaX, deltaY) {
1945
+ // Swiping toward the axis start edge (down/right) is allowed when scrolled to the start;
1946
+ // toward the end edge (up/left) when scrolled to the end.
1947
+ const canSwipeOnAxis = (delta, scrollOffset, maxScrollOffset, allowTowardStart, allowTowardEnd) => delta > 0 && scrollOffset <= 0 && allowTowardStart || delta < 0 && scrollOffset >= Math.max(0, maxScrollOffset) && allowTowardEnd;
2019
1948
  const absDeltaX = Math.abs(deltaX);
2020
1949
  const absDeltaY = Math.abs(deltaY);
2021
- const useVerticalAxis = hasVertical && deltaY !== 0 && (!hasHorizontal || absDeltaY >= absDeltaX);
2022
- if (useVerticalAxis) {
2023
- const maxScrollTop = Math.max(0, scrollTarget.scrollHeight - scrollTarget.clientHeight);
2024
- const atTop = scrollTarget.scrollTop <= 0;
2025
- const atBottom = scrollTarget.scrollTop >= maxScrollTop;
2026
- const movingDown = deltaY > 0;
2027
- const movingUp = deltaY < 0;
2028
- const canSwipeDown = movingDown && atTop && allowDown;
2029
- const canSwipeUp = movingUp && atBottom && allowUp;
2030
- return canSwipeDown || canSwipeUp;
2031
- }
2032
- const useHorizontalAxis = hasHorizontal && deltaX !== 0 && (!hasVertical || absDeltaX > absDeltaY);
2033
- if (useHorizontalAxis) {
2034
- const maxScrollLeft = Math.max(0, scrollTarget.scrollWidth - scrollTarget.clientWidth);
2035
- const atLeft = scrollTarget.scrollLeft <= 0;
2036
- const atRight = scrollTarget.scrollLeft >= maxScrollLeft;
2037
- const movingRight = deltaX > 0;
2038
- const movingLeft = deltaX < 0;
2039
- const canSwipeRight = movingRight && atLeft && allowRight;
2040
- const canSwipeLeft = movingLeft && atRight && allowLeft;
2041
- return canSwipeRight || canSwipeLeft;
1950
+ if (hasVertical && deltaY !== 0 && (!hasHorizontal || absDeltaY >= absDeltaX)) {
1951
+ return canSwipeOnAxis(deltaY, scrollTarget.scrollTop, scrollTarget.scrollHeight - scrollTarget.clientHeight, allowDown, allowUp);
1952
+ }
1953
+ if (hasHorizontal && deltaX !== 0 && (!hasVertical || absDeltaX > absDeltaY)) {
1954
+ return canSwipeOnAxis(deltaX, scrollTarget.scrollLeft, scrollTarget.scrollWidth - scrollTarget.clientWidth, allowRight, allowLeft);
2042
1955
  }
2043
1956
  return null;
2044
1957
  }
@@ -2059,7 +1972,7 @@ function useSwipeDismiss(options) {
2059
1972
  pendingSwipeRef.current = true;
2060
1973
  pendingSwipeStartPosRef.current = startPos;
2061
1974
  swipeFromScrollableRef.current = false;
2062
- sawPrimaryButtonsOnMoveRef.current = false;
1975
+ sawPrimaryButtonsOnMoveRef.current = !('touches' in event);
2063
1976
  const allowedToStart = canStart ? canStart(startPos, {
2064
1977
  nativeEvent: event.nativeEvent,
2065
1978
  direction: primaryDirection
@@ -2087,14 +2000,21 @@ function useSwipeDismiss(options) {
2087
2000
  event.preventDefault();
2088
2001
  }
2089
2002
  if (isFirstPointerMoveRef.current) {
2090
- // Adjust the starting position to the current position on the first move
2091
- // to account for the delay between pointerdown and the first pointermove on iOS.
2092
- dragStartPosRef.current = position;
2093
- const moveTime = getValidTimeStamp(event.timeStamp);
2094
- if (moveTime !== null) {
2095
- swipeStartTimeRef.current = moveTime;
2096
- }
2097
2003
  isFirstPointerMoveRef.current = false;
2004
+ // Reset the drag origin to the first move's position to absorb the gap between the press and
2005
+ // the first move event — notably on iOS touch, where the first `touchmove` arrives already
2006
+ // offset from the `touchstart` — which would otherwise make the dragged element jump. This
2007
+ // only matters when an element follows the pointer; when `trackDrag` is false (e.g. the
2008
+ // swipe-area, which only opens the drawer) keep the original press position so a quick flick
2009
+ // still registers: on a low-refresh-rate display the whole travel can land in this single
2010
+ // first move, and discarding it would drop the gesture.
2011
+ if (trackDrag) {
2012
+ dragStartPosRef.current = position;
2013
+ const moveTime = getValidTimeStamp(event.timeStamp);
2014
+ if (moveTime !== null) {
2015
+ swipeStartTimeRef.current = moveTime;
2016
+ }
2017
+ }
2098
2018
  }
2099
2019
  const clientX = position.x;
2100
2020
  const clientY = position.y;
@@ -2182,51 +2102,45 @@ function useSwipeDismiss(options) {
2182
2102
  newOffsetY += dampedDelta.y;
2183
2103
  }
2184
2104
  }
2105
+
2106
+ // Only rewrite drag styles when the drag offset actually changed. `syncDragStyles` writes the
2107
+ // raw (undamped) frozen transform and movement vars, relying on the consumer's `onProgress`
2108
+ // to overwrite them with damped styles — but `updateSwipeProgress` dedupes unchanged
2109
+ // deltas and skips `onProgress`. A move that doesn't change the offset (e.g. the cursor
2110
+ // pinned at a screen edge during an off-screen drag, jittering only on the ignored axis)
2111
+ // would otherwise reinstate the raw styles with no correction, jumping the element to the
2112
+ // undamped position.
2113
+ const previousOffset = dragOffsetRef.current;
2114
+ const offsetChanged = newOffsetX !== previousOffset.x || newOffsetY !== previousOffset.y;
2185
2115
  dragOffsetRef.current = {
2186
2116
  x: newOffsetX,
2187
2117
  y: newOffsetY
2188
2118
  };
2189
- syncDragStyles(true);
2119
+ if (offsetChanged) {
2120
+ syncDragStyles(true);
2121
+ }
2190
2122
  recordDragSample({
2191
2123
  x: newOffsetX,
2192
2124
  y: newOffsetY
2193
2125
  }, getValidTimeStamp(event.timeStamp));
2194
2126
  const dragDeltaX = newOffsetX - initialTransformRef.current.x;
2195
2127
  const dragDeltaY = newOffsetY - initialTransformRef.current.y;
2196
- const swipeDirectionDetails = intendedSwipeDirectionRef.current;
2197
- const progressDirection = primaryDirection ?? intendedSwipeDirectionRef.current;
2198
- if (!progressDirection) {
2199
- updateSwipeProgress(0, {
2200
- deltaX: dragDeltaX,
2201
- deltaY: dragDeltaY,
2202
- direction: swipeDirectionDetails
2203
- });
2204
- return;
2205
- }
2206
- const size = progressDirection === 'left' || progressDirection === 'right' ? elementSizeRef.current.width : elementSizeRef.current.height;
2207
- const scale = initialTransformRef.current.scale || 1;
2208
- if (size <= 0 || scale <= 0) {
2209
- updateSwipeProgress(0, {
2210
- deltaX: dragDeltaX,
2211
- deltaY: dragDeltaY,
2212
- direction: swipeDirectionDetails
2213
- });
2214
- return;
2215
- }
2216
- const progressDisplacement = getDisplacement(progressDirection, newOffsetX - initialTransformRef.current.x, newOffsetY - initialTransformRef.current.y);
2217
- if (progressDisplacement <= 0) {
2218
- updateSwipeProgress(0, {
2219
- deltaX: dragDeltaX,
2220
- deltaY: dragDeltaY,
2221
- direction: swipeDirectionDetails
2222
- });
2223
- return;
2224
- }
2225
- updateSwipeProgress(progressDisplacement / (size * scale), {
2128
+ const progressDetails = {
2226
2129
  deltaX: dragDeltaX,
2227
2130
  deltaY: dragDeltaY,
2228
- direction: swipeDirectionDetails
2229
- });
2131
+ direction: intendedSwipeDirectionRef.current
2132
+ };
2133
+ let progress = 0;
2134
+ const progressDirection = primaryDirection ?? intendedSwipeDirectionRef.current;
2135
+ if (progressDirection) {
2136
+ const size = progressDirection === 'left' || progressDirection === 'right' ? elementSizeRef.current.width : elementSizeRef.current.height;
2137
+ const scale = initialTransformRef.current.scale || 1;
2138
+ const progressDisplacement = getDisplacement(progressDirection, dragDeltaX, dragDeltaY);
2139
+ if (size > 0 && scale > 0 && progressDisplacement > 0) {
2140
+ progress = progressDisplacement / (size * scale);
2141
+ }
2142
+ }
2143
+ updateSwipeProgress(progress, progressDetails);
2230
2144
  }
2231
2145
  const handleEnd = useScrollLock.useStableCallback(event => {
2232
2146
  if (!enabled) {
@@ -2314,33 +2228,9 @@ function useSwipeDismiss(options) {
2314
2228
  dismissDirection = intendedSwipeDirectionRef.current ?? primaryDirection;
2315
2229
  } else {
2316
2230
  for (const direction of directions) {
2317
- switch (direction) {
2318
- case 'right':
2319
- if (deltaX > swipeThresholdRef.current) {
2320
- shouldClose = true;
2321
- dismissDirection = 'right';
2322
- }
2323
- break;
2324
- case 'left':
2325
- if (deltaX < -swipeThresholdRef.current) {
2326
- shouldClose = true;
2327
- dismissDirection = 'left';
2328
- }
2329
- break;
2330
- case 'down':
2331
- if (deltaY > swipeThresholdRef.current) {
2332
- shouldClose = true;
2333
- dismissDirection = 'down';
2334
- }
2335
- break;
2336
- case 'up':
2337
- if (deltaY < -swipeThresholdRef.current) {
2338
- shouldClose = true;
2339
- dismissDirection = 'up';
2340
- }
2341
- break;
2342
- }
2343
- if (shouldClose) {
2231
+ if (getDisplacement(direction, deltaX, deltaY) > swipeThresholdRef.current) {
2232
+ shouldClose = true;
2233
+ dismissDirection = direction;
2344
2234
  break;
2345
2235
  }
2346
2236
  }
@@ -2482,25 +2372,29 @@ function useSwipeDismiss(options) {
2482
2372
  });
2483
2373
  });
2484
2374
  const getDragStyles = React__namespace.useCallback(() => {
2375
+ // Read `isSwipingRef`, not the lagging `isSwiping` state, to match the imperative writer
2376
+ // `syncDragStyles`. Otherwise a render that commits before `setSwiping(true)` flushes strips the
2377
+ // transform it just wrote, flashing the popup to its resting position for a frame.
2378
+ const swiping = isSwipingRef.current;
2485
2379
  const dragOffset = dragOffsetRef.current;
2486
2380
  const initialTransform = initialTransformRef.current;
2487
2381
  const deltaX = dragOffset.x - initialTransform.x;
2488
2382
  const deltaY = dragOffset.y - initialTransform.y;
2489
- if (!isSwiping && deltaX === 0 && deltaY === 0 && !dragDismissed) {
2383
+ if (!swiping && deltaX === 0 && deltaY === 0 && !dragDismissed) {
2490
2384
  return {
2491
2385
  [movementCssVars.x]: '0px',
2492
2386
  [movementCssVars.y]: '0px'
2493
2387
  };
2494
2388
  }
2495
2389
  return {
2496
- transition: isSwiping ? 'none' : undefined,
2390
+ transition: swiping ? 'none' : undefined,
2497
2391
  // While swiping, freeze the element at its current visual transform so it doesn't snap to the
2498
2392
  // end position.
2499
- transform: isSwiping ? getDragTransform(dragOffset, initialTransform.scale) : undefined,
2393
+ transform: swiping ? getDragTransform(dragOffset, initialTransform.scale) : undefined,
2500
2394
  [movementCssVars.x]: `${deltaX}px`,
2501
2395
  [movementCssVars.y]: `${deltaY}px`
2502
2396
  };
2503
- }, [dragDismissed, isSwiping, movementCssVars]);
2397
+ }, [dragDismissed, movementCssVars]);
2504
2398
  const getPointerProps = React__namespace.useCallback(() => {
2505
2399
  if (!enabled) {
2506
2400
  return {};
@@ -2560,6 +2454,8 @@ const MIN_SWIPE_RELEASE_DURATION_MS = 80;
2560
2454
  const MAX_SWIPE_RELEASE_DURATION_MS = 360;
2561
2455
  const MIN_SWIPE_RELEASE_SCALAR = 0.1;
2562
2456
  const MAX_SWIPE_RELEASE_SCALAR = 1;
2457
+ const AXIS_LOCK_SLOP = 6;
2458
+ const AXIS_LOCK_BIAS = 2;
2563
2459
  const DRAWER_CONTENT_SELECTOR = `[${DRAWER_CONTENT_ATTRIBUTE}]`;
2564
2460
  /**
2565
2461
  * A positioning container for the drawer popup that can be made scrollable.
@@ -2575,9 +2471,7 @@ const DrawerViewport = /*#__PURE__*/React__namespace.forwardRef(function DrawerV
2575
2471
  children,
2576
2472
  ...elementProps
2577
2473
  } = props;
2578
- const {
2579
- store
2580
- } = useDialogRootContext();
2474
+ const store = useDialogRootContext();
2581
2475
  const popupRef = store.context.popupRef;
2582
2476
  const backdropRef = store.context.backdropRef;
2583
2477
  const {
@@ -2585,7 +2479,8 @@ const DrawerViewport = /*#__PURE__*/React__namespace.forwardRef(function DrawerV
2585
2479
  notifyParentSwipingChange,
2586
2480
  notifyParentSwipeProgressChange,
2587
2481
  frontmostHeight,
2588
- snapToSequentialPoints
2482
+ snapToSequentialPoints,
2483
+ swipeAreaActiveRef
2589
2484
  } = useDrawerRootContext();
2590
2485
  const providerContext = useDrawerProviderContext();
2591
2486
  const {
@@ -2619,30 +2514,13 @@ const DrawerViewport = /*#__PURE__*/React__namespace.forwardRef(function DrawerV
2619
2514
  const touchScrollStateRef = React__namespace.useRef(null);
2620
2515
  const virtualKeyboard = useDrawerVirtualKeyboardContext();
2621
2516
  const snapPointRange = React__namespace.useMemo(() => {
2622
- if (!snapPoints || snapPoints.length < 2) {
2623
- return null;
2624
- }
2625
- if (swipeDirection !== 'down' && swipeDirection !== 'up') {
2626
- return null;
2627
- }
2628
- if (resolvedSnapPoints.length < 2) {
2629
- return null;
2630
- }
2631
- const offsets = resolvedSnapPoints.map(point => point.offset).filter(offset => Number.isFinite(offset)).sort((a, b) => a - b);
2632
- if (offsets.length < 2) {
2517
+ if (!snapPoints || snapPoints.length < 2 || resolvedSnapPoints.length < 2 || swipeDirection !== 'down' && swipeDirection !== 'up') {
2633
2518
  return null;
2634
2519
  }
2520
+ const offsets = resolvedSnapPoints.map(point => point.offset).sort((a, b) => a - b);
2635
2521
  const minOffset = offsets[0];
2636
2522
  const nextOffset = offsets[1];
2637
- const maxOffset = offsets[offsets.length - 1];
2638
- let range = nextOffset - minOffset;
2639
- if (!Number.isFinite(range) || range <= 0) {
2640
- const fallbackRange = maxOffset - minOffset;
2641
- if (!Number.isFinite(fallbackRange) || fallbackRange <= 0) {
2642
- return null;
2643
- }
2644
- range = fallbackRange;
2645
- }
2523
+ const range = nextOffset - minOffset;
2646
2524
  return {
2647
2525
  minOffset,
2648
2526
  range
@@ -2661,7 +2539,8 @@ const DrawerViewport = /*#__PURE__*/React__namespace.forwardRef(function DrawerV
2661
2539
  return [swipeDirection];
2662
2540
  }, [snapPoints, swipeDirection]);
2663
2541
  const setSwipeDismissed = useScrollLock.useStableCallback(dismissed => {
2664
- setSwipeDismissedElements(popupRef.current, backdropRef.current, dismissed);
2542
+ popupRef.current?.toggleAttribute(DrawerPopupDataAttributes.swipeDismiss, dismissed);
2543
+ backdropRef.current?.toggleAttribute(DrawerPopupDataAttributes.swipeDismiss, dismissed);
2665
2544
  });
2666
2545
  const clearSwipeRelease = useScrollLock.useStableCallback(() => {
2667
2546
  setSwipeDismissed(false);
@@ -2675,11 +2554,7 @@ const DrawerViewport = /*#__PURE__*/React__namespace.forwardRef(function DrawerV
2675
2554
  nestedSwipeActiveRef.current = false;
2676
2555
  notifyParentSwipingChange?.(false);
2677
2556
  });
2678
- const applySwipeProgress = useScrollLock.useStableCallback(({
2679
- resolvedProgress,
2680
- shouldTrackProgress,
2681
- notifyParent
2682
- }) => {
2557
+ const applySwipeProgress = useScrollLock.useStableCallback((resolvedProgress, shouldTrackProgress, notifyParent) => {
2683
2558
  const isActive = open && !nested && shouldTrackProgress;
2684
2559
  const swipeProgress = isActive ? resolvedProgress : 0;
2685
2560
  const nestedSwipeProgress = open && shouldTrackProgress ? resolvedProgress : 0;
@@ -2697,78 +2572,48 @@ const DrawerViewport = /*#__PURE__*/React__namespace.forwardRef(function DrawerV
2697
2572
  if (!backdropElement) {
2698
2573
  return;
2699
2574
  }
2700
- if (!isActive || swipeProgress <= 0) {
2701
- backdropElement.style.setProperty(DrawerBackdropCssVars.swipeProgress, '0');
2702
- backdropElement.style.removeProperty(DrawerPopupCssVars.height);
2703
- return;
2704
- }
2705
- backdropElement.style.setProperty(DrawerBackdropCssVars.swipeProgress, `${swipeProgress}`);
2706
- if (frontmostHeight > 0) {
2575
+ const showProgress = isActive && swipeProgress > 0;
2576
+ backdropElement.style.setProperty(DrawerBackdropCssVars.swipeProgress, showProgress ? `${swipeProgress}` : '0');
2577
+ if (showProgress && frontmostHeight > 0) {
2707
2578
  backdropElement.style.setProperty(DrawerPopupCssVars.height, `${frontmostHeight}px`);
2708
2579
  } else {
2709
2580
  backdropElement.style.removeProperty(DrawerPopupCssVars.height);
2710
2581
  }
2711
2582
  });
2712
- function resolveSwipeRelease({
2713
- direction,
2714
- deltaX,
2715
- deltaY,
2716
- velocityX,
2717
- velocityY,
2718
- releaseVelocityX,
2719
- releaseVelocityY
2720
- }) {
2721
- if (!direction) {
2722
- return null;
2723
- }
2724
- const popupElement = store.context.popupRef.current;
2725
- if (!popupElement) {
2726
- return null;
2727
- }
2728
- const size = direction === 'left' || direction === 'right' ? popupElement.offsetWidth : popupElement.offsetHeight;
2729
- if (!Number.isFinite(size) || size <= 0) {
2583
+ function resolveSwipeRelease(popupElement, direction, deltaX, deltaY, velocityX, velocityY, releaseVelocityX, releaseVelocityY) {
2584
+ const size = getBaseSwipeSize(popupElement, direction);
2585
+ if (size <= 0) {
2730
2586
  return null;
2731
2587
  }
2732
- const axisDelta = direction === 'left' || direction === 'right' ? deltaX : deltaY;
2733
- const snapPointBaseOffset = snapPoints && snapPoints.length > 0 ? activeSnapPointOffset ?? 0 : 0;
2734
- let baseOffset = 0;
2735
- if (direction === 'down') {
2736
- baseOffset = snapPointBaseOffset;
2737
- } else if (direction === 'up') {
2738
- baseOffset = -snapPointBaseOffset;
2739
- }
2740
- const translation = baseOffset + axisDelta;
2741
- const translationAlongDirection = direction === 'left' || direction === 'up' ? -translation : translation;
2588
+
2589
+ // The snap point base offset shifts the popup along the dismiss direction for both
2590
+ // `down` (+offset) and `up` (-offset), so it always adds to the directional translation.
2591
+ const snapPointBaseOffset = (direction === 'down' || direction === 'up') && snapPoints && snapPoints.length > 0 ? activeSnapPointOffset ?? 0 : 0;
2592
+ const translationAlongDirection = snapPointBaseOffset + getDisplacement(direction, deltaX, deltaY);
2742
2593
  const remainingDistance = Math.max(0, size - translationAlongDirection);
2743
- if (!Number.isFinite(remainingDistance) || remainingDistance <= 0) {
2594
+ if (remainingDistance <= 0) {
2744
2595
  return null;
2745
2596
  }
2746
- const axisVelocity = direction === 'left' || direction === 'right' ? releaseVelocityX : releaseVelocityY;
2747
- const fallbackVelocity = direction === 'left' || direction === 'right' ? velocityX : velocityY;
2748
- const resolvedVelocity = Math.abs(axisVelocity) > 0 && Number.isFinite(axisVelocity) ? axisVelocity : fallbackVelocity;
2749
- const directionalVelocity = direction === 'left' || direction === 'up' ? -resolvedVelocity : resolvedVelocity;
2750
- if (!Number.isFinite(directionalVelocity) || directionalVelocity <= MIN_SWIPE_RELEASE_VELOCITY) {
2597
+ const releaseVelocity = getDisplacement(direction, releaseVelocityX, releaseVelocityY);
2598
+ const directionalVelocity = Math.abs(releaseVelocity) > 0 ? releaseVelocity : getDisplacement(direction, velocityX, velocityY);
2599
+ if (directionalVelocity <= MIN_SWIPE_RELEASE_VELOCITY) {
2751
2600
  return null;
2752
2601
  }
2753
2602
  const clampedVelocity = scrollEdges.clamp(directionalVelocity, MIN_SWIPE_RELEASE_VELOCITY, MAX_SWIPE_RELEASE_VELOCITY);
2603
+ // The gesture hook supplies finite deltas and velocities. The guards above keep the remaining
2604
+ // distance and divisor positive, so the duration stays within [MIN, MAX] and the resulting
2605
+ // scalar within (0, 1].
2754
2606
  const durationMs = scrollEdges.clamp(remainingDistance / clampedVelocity, MIN_SWIPE_RELEASE_DURATION_MS, MAX_SWIPE_RELEASE_DURATION_MS);
2755
- if (!Number.isFinite(durationMs)) {
2756
- return null;
2757
- }
2758
2607
  const normalizedDuration = (durationMs - MIN_SWIPE_RELEASE_DURATION_MS) / (MAX_SWIPE_RELEASE_DURATION_MS - MIN_SWIPE_RELEASE_DURATION_MS);
2759
- const durationScalar = scrollEdges.clamp(MIN_SWIPE_RELEASE_SCALAR + normalizedDuration * (MAX_SWIPE_RELEASE_SCALAR - MIN_SWIPE_RELEASE_SCALAR), MIN_SWIPE_RELEASE_SCALAR, MAX_SWIPE_RELEASE_SCALAR);
2760
- if (!Number.isFinite(durationScalar) || durationScalar <= 0) {
2761
- return null;
2762
- }
2763
- return durationScalar;
2608
+ return MIN_SWIPE_RELEASE_SCALAR + normalizedDuration * (MAX_SWIPE_RELEASE_SCALAR - MIN_SWIPE_RELEASE_SCALAR);
2764
2609
  }
2765
2610
  function updateNestedSwipeActive(details) {
2766
2611
  if (nestedSwipeActiveRef.current || !details) {
2767
2612
  return;
2768
2613
  }
2769
2614
  const direction = details.direction ?? swipeDirection;
2770
- const delta = direction === 'left' || direction === 'right' ? details.deltaX : details.deltaY;
2771
- if (!Number.isFinite(delta) || Math.abs(delta) < MIN_SWIPE_THRESHOLD) {
2615
+ const delta = getDisplacement(direction, details.deltaX, details.deltaY);
2616
+ if (Math.abs(delta) < MIN_SWIPE_THRESHOLD) {
2772
2617
  return;
2773
2618
  }
2774
2619
  nestedSwipeActiveRef.current = true;
@@ -2785,20 +2630,17 @@ const DrawerViewport = /*#__PURE__*/React__namespace.forwardRef(function DrawerV
2785
2630
  y: DrawerPopupCssVars.swipeMovementY
2786
2631
  },
2787
2632
  onSwipeStart(event) {
2788
- if ('touches' in event || 'pointerType' in event && event.pointerType === 'touch') {
2633
+ if ('touches' in event || event.pointerType === 'touch') {
2789
2634
  return;
2790
2635
  }
2791
2636
  const popupElement = popupRef.current;
2792
- if (!popupElement) {
2793
- return;
2794
- }
2795
2637
  const doc = useScrollLock.ownerDocument(popupElement);
2796
2638
  const selection = doc.getSelection?.();
2797
2639
  if (!selection || selection.isCollapsed) {
2798
2640
  return;
2799
2641
  }
2800
- const anchorElement = floatingUi_utils.isElement(selection.anchorNode) ? selection.anchorNode : selection.anchorNode?.parentElement;
2801
- const focusElement = floatingUi_utils.isElement(selection.focusNode) ? selection.focusNode : selection.focusNode?.parentElement;
2642
+ const anchorElement = useScrollLock.isElement(selection.anchorNode) ? selection.anchorNode : selection.anchorNode?.parentElement;
2643
+ const focusElement = useScrollLock.isElement(selection.focusNode) ? selection.focusNode : selection.focusNode?.parentElement;
2802
2644
  if (!useScrollLock.contains(popupElement, anchorElement) && !useScrollLock.contains(popupElement, focusElement)) {
2803
2645
  return;
2804
2646
  }
@@ -2823,54 +2665,38 @@ const DrawerViewport = /*#__PURE__*/React__namespace.forwardRef(function DrawerV
2823
2665
  return false;
2824
2666
  }
2825
2667
  const doc = popupElement.ownerDocument;
2826
- const elementAtPoint = getElementAtPoint(doc, position.x, position.y);
2668
+ const elementAtPoint = getElementAtPoint(popupElement.getRootNode(), position.x, position.y);
2827
2669
  if (!elementAtPoint || !useScrollLock.contains(popupElement, elementAtPoint)) {
2828
2670
  return false;
2829
2671
  }
2830
2672
  const nativeEvent = details.nativeEvent;
2831
- const touchLike = 'touches' in nativeEvent || 'pointerType' in nativeEvent && nativeEvent.pointerType === 'touch';
2673
+ const touchLike = 'touches' in nativeEvent || nativeEvent.pointerType === 'touch';
2832
2674
  if (touchLike && shouldIgnoreSwipeForTextSelection(doc, popupElement)) {
2833
2675
  return false;
2834
2676
  }
2835
- if (nativeEvent.type === 'touchstart' && isSwipeIgnoredTarget(elementAtPoint)) {
2836
- return false;
2837
- }
2838
2677
  return true;
2839
2678
  },
2840
2679
  onProgress(progress, details) {
2841
2680
  updateNestedSwipeActive(details);
2842
2681
  const hasSnapPoints = Boolean(snapPoints && snapPoints.length > 0);
2843
- if (swipingRef.current && swipeDirection === 'down' && hasSnapPoints && details && Number.isFinite(details.deltaY)) {
2682
+ if (swipingRef.current && swipeDirection === 'down' && hasSnapPoints && details) {
2844
2683
  const popupElement = store.context.popupRef.current;
2845
2684
  if (popupElement) {
2846
2685
  popupElement.style.removeProperty('transform');
2847
2686
  popupElement.style.setProperty(DrawerPopupCssVars.swipeMovementY, `${getSnapPointSwipeMovement(activeSnapPointOffset ?? 0, details.deltaY)}px`);
2848
2687
  }
2849
2688
  }
2850
- const currentDirection = details?.direction ?? swipe.swipeDirection;
2851
- const isDismissSwipe = currentDirection === undefined || currentDirection === swipeDirection;
2852
- const isVerticalSwipe = swipeDirection === 'down' || swipeDirection === 'up';
2853
- const shouldTrackProgress = hasSnapPoints && isVerticalSwipe || !hasSnapPoints || swipeDirection === 'left' || swipeDirection === 'right' || isDismissSwipe;
2854
2689
  let resolvedProgress = progress;
2855
2690
  if (snapPointRange && popupHeight > 0) {
2691
+ const baseOffset = activeSnapPointOffset ?? snapPointRange.minOffset;
2692
+ const offsetToProgress = nextOffset => scrollEdges.clamp((nextOffset - snapPointRange.minOffset) / snapPointRange.range, 0, 1);
2856
2693
  if (details && Number.isFinite(details.deltaY)) {
2857
- const baseOffset = activeSnapPointOffset ?? snapPointRange.minOffset;
2858
- const nextOffset = scrollEdges.clamp(baseOffset + details.deltaY, 0, popupHeight);
2859
- resolvedProgress = scrollEdges.clamp((nextOffset - snapPointRange.minOffset) / snapPointRange.range, 0, 1);
2694
+ resolvedProgress = offsetToProgress(scrollEdges.clamp(baseOffset + details.deltaY, 0, popupHeight));
2860
2695
  } else if (snapPointProgress !== null) {
2861
2696
  resolvedProgress = snapPointProgress;
2862
- } else if (currentDirection === 'down' || currentDirection === 'up') {
2863
- const displacement = progress * popupHeight;
2864
- const baseOffset = activeSnapPointOffset ?? snapPointRange.minOffset;
2865
- const nextOffset = currentDirection === 'down' ? baseOffset + displacement : baseOffset - displacement;
2866
- resolvedProgress = scrollEdges.clamp((nextOffset - snapPointRange.minOffset) / snapPointRange.range, 0, 1);
2867
2697
  }
2868
2698
  }
2869
- applySwipeProgress({
2870
- resolvedProgress,
2871
- shouldTrackProgress,
2872
- notifyParent: true
2873
- });
2699
+ applySwipeProgress(resolvedProgress, true, true);
2874
2700
  },
2875
2701
  onRelease({
2876
2702
  event,
@@ -2882,30 +2708,21 @@ const DrawerViewport = /*#__PURE__*/React__namespace.forwardRef(function DrawerV
2882
2708
  releaseVelocityX,
2883
2709
  releaseVelocityY
2884
2710
  }) {
2885
- const swipeReleasePayload = {
2886
- deltaX,
2887
- deltaY,
2888
- velocityX,
2889
- velocityY,
2890
- releaseVelocityX,
2891
- releaseVelocityY
2892
- };
2711
+ const popupElement = store.context.popupRef.current;
2712
+ if (!popupElement) {
2713
+ clearSwipeRelease();
2714
+ return undefined;
2715
+ }
2716
+ const releasePopupElement = popupElement;
2893
2717
  function startSwipeRelease(resolvedDirection) {
2894
2718
  // Start ending transition styles earlier and synchronously to prevent a period where
2895
2719
  // the popup appears stuck on release before the actual closing animation starts.
2896
- const popupElement = store.context.popupRef.current;
2897
- if (!popupElement) {
2898
- return;
2899
- }
2900
2720
  finishNestedSwipe();
2901
2721
  setSwipeDismissed(true);
2902
- popupElement.style.removeProperty('transition');
2903
- popupElement.setAttribute(useScrollLock.TransitionStatusDataAttributes.endingStyle, '');
2722
+ releasePopupElement.style.removeProperty('transition');
2723
+ releasePopupElement.setAttribute(useScrollLock.TransitionStatusDataAttributes.endingStyle, '');
2904
2724
  ReactDOM__namespace.flushSync(() => {
2905
- setSwipeRelease(resolveSwipeRelease({
2906
- direction: resolvedDirection,
2907
- ...swipeReleasePayload
2908
- }));
2725
+ setSwipeRelease(resolveSwipeRelease(releasePopupElement, resolvedDirection, deltaX, deltaY, velocityX, velocityY, releaseVelocityX, releaseVelocityY));
2909
2726
  });
2910
2727
  }
2911
2728
  if (!snapPoints || snapPoints.length === 0) {
@@ -2913,29 +2730,16 @@ const DrawerViewport = /*#__PURE__*/React__namespace.forwardRef(function DrawerV
2913
2730
  clearSwipeRelease();
2914
2731
  return undefined;
2915
2732
  }
2916
- const element = store.context.popupRef.current;
2917
- if (!element) {
2918
- clearSwipeRelease();
2919
- return undefined;
2920
- }
2921
- const baseThreshold = getBaseSwipeThreshold(element, direction);
2922
- const delta = direction === 'left' || direction === 'right' ? deltaX : deltaY;
2923
- if (!Number.isFinite(delta)) {
2924
- clearSwipeRelease();
2925
- return undefined;
2926
- }
2927
- const directionalDelta = direction === 'left' || direction === 'up' ? -delta : delta;
2733
+ const directionalDelta = getDisplacement(direction, deltaX, deltaY);
2928
2734
  if (directionalDelta <= 0) {
2929
2735
  clearSwipeRelease();
2930
2736
  return false;
2931
2737
  }
2932
- const velocity = direction === 'left' || direction === 'right' ? velocityX : velocityY;
2933
- const directionalVelocity = direction === 'left' || direction === 'up' ? -velocity : velocity;
2934
- if (directionalVelocity >= FAST_SWIPE_VELOCITY && directionalDelta > 0) {
2738
+ if (getDisplacement(direction, velocityX, velocityY) >= FAST_SWIPE_VELOCITY) {
2935
2739
  startSwipeRelease(direction);
2936
2740
  return true;
2937
2741
  }
2938
- const shouldClose = directionalDelta > baseThreshold;
2742
+ const shouldClose = directionalDelta > getBaseSwipeThreshold(releasePopupElement, direction);
2939
2743
  if (shouldClose) {
2940
2744
  startSwipeRelease(direction);
2941
2745
  } else {
@@ -2947,20 +2751,20 @@ const DrawerViewport = /*#__PURE__*/React__namespace.forwardRef(function DrawerV
2947
2751
  clearSwipeRelease();
2948
2752
  return undefined;
2949
2753
  }
2950
- if (!popupHeight || resolvedSnapPoints.length === 0) {
2754
+ if (!popupHeight) {
2951
2755
  clearSwipeRelease();
2952
- return undefined;
2756
+ return false;
2953
2757
  }
2954
- const dragDelta = swipeDirection === 'down' ? deltaY : -deltaY;
2955
- if (!Number.isFinite(dragDelta)) {
2758
+ if (resolvedSnapPoints.length === 0) {
2956
2759
  clearSwipeRelease();
2957
2760
  return undefined;
2958
2761
  }
2762
+ const dragDelta = swipeDirection === 'down' ? deltaY : -deltaY;
2959
2763
  const dragDirection = Math.sign(dragDelta);
2960
2764
  const releaseDirectionalVelocity = swipeDirection === 'down' ? releaseVelocityY : -releaseVelocityY;
2961
2765
  const fallbackDirectionalVelocity = swipeDirection === 'down' ? velocityY : -velocityY;
2962
- let resolvedDirectionalVelocity = Number.isFinite(releaseDirectionalVelocity) ? releaseDirectionalVelocity : fallbackDirectionalVelocity;
2963
- if (dragDirection !== 0 && Math.abs(dragDelta) >= MIN_SWIPE_THRESHOLD && Number.isFinite(resolvedDirectionalVelocity)) {
2766
+ let resolvedDirectionalVelocity = releaseDirectionalVelocity;
2767
+ if (dragDirection !== 0 && Math.abs(dragDelta) >= MIN_SWIPE_THRESHOLD) {
2964
2768
  const velocityDirection = Math.sign(resolvedDirectionalVelocity);
2965
2769
  if (velocityDirection !== 0 && velocityDirection !== dragDirection) {
2966
2770
  // Ignore touch reversals that would otherwise flip the snap decision.
@@ -2969,39 +2773,20 @@ const DrawerViewport = /*#__PURE__*/React__namespace.forwardRef(function DrawerV
2969
2773
  }
2970
2774
  const currentOffset = activeSnapPointOffset ?? 0;
2971
2775
  const dragTargetOffset = scrollEdges.clamp(currentOffset + dragDelta, 0, popupHeight);
2972
- const velocityOffset = Number.isFinite(resolvedDirectionalVelocity) && Math.abs(resolvedDirectionalVelocity) >= SNAP_VELOCITY_THRESHOLD ? scrollEdges.clamp(resolvedDirectionalVelocity, -MAX_SNAP_VELOCITY, MAX_SNAP_VELOCITY) * SNAP_VELOCITY_MULTIPLIER : 0;
2776
+ const velocityOffset = Math.abs(resolvedDirectionalVelocity) >= SNAP_VELOCITY_THRESHOLD ? scrollEdges.clamp(resolvedDirectionalVelocity, -MAX_SNAP_VELOCITY, MAX_SNAP_VELOCITY) * SNAP_VELOCITY_MULTIPLIER : 0;
2973
2777
  const targetOffset = snapToSequentialPoints ? dragTargetOffset : scrollEdges.clamp(dragTargetOffset + velocityOffset, 0, popupHeight);
2974
2778
  const snapPointEventDetails = useScrollLock.createChangeEventDetails(useScrollLock.swipe, event);
2975
2779
  const closeFromSnapPoints = () => {
2976
2780
  pendingSwipeCloseSnapPointRef.current = activeSnapPoint;
2977
- setActiveSnapPoint?.(null, snapPointEventDetails);
2781
+ setActiveSnapPoint(null, snapPointEventDetails);
2978
2782
  startSwipeRelease(swipeDirection);
2979
2783
  return true;
2980
2784
  };
2981
2785
  if (snapToSequentialPoints) {
2982
2786
  const orderedSnapPoints = [...resolvedSnapPoints].sort((first, second) => first.offset - second.offset);
2983
- if (orderedSnapPoints.length === 0) {
2984
- clearSwipeRelease();
2985
- return false;
2986
- }
2987
- let currentIndex = 0;
2988
- let closestDistance = Math.abs(currentOffset - orderedSnapPoints[0].offset);
2989
- for (let index = 1; index < orderedSnapPoints.length; index += 1) {
2990
- const distance = Math.abs(currentOffset - orderedSnapPoints[index].offset);
2991
- if (distance < closestDistance) {
2992
- closestDistance = distance;
2993
- currentIndex = index;
2994
- }
2995
- }
2996
- let targetSnapPoint = orderedSnapPoints[0];
2997
- closestDistance = Math.abs(targetOffset - targetSnapPoint.offset);
2998
- for (const snapPoint of orderedSnapPoints) {
2999
- const distance = Math.abs(targetOffset - snapPoint.offset);
3000
- if (distance < closestDistance) {
3001
- closestDistance = distance;
3002
- targetSnapPoint = snapPoint;
3003
- }
3004
- }
2787
+ const orderedOffsets = orderedSnapPoints.map(point => point.offset);
2788
+ const currentIndex = closestSnapPointIndex(orderedOffsets, currentOffset);
2789
+ let targetSnapPoint = orderedSnapPoints[closestSnapPointIndex(orderedOffsets, targetOffset)];
3005
2790
  const velocityDirection = Math.sign(resolvedDirectionalVelocity);
3006
2791
  const shouldAdvance = dragDirection !== 0 && velocityDirection !== 0 && velocityDirection === dragDirection && Math.abs(resolvedDirectionalVelocity) >= SNAP_VELOCITY_THRESHOLD;
3007
2792
  let effectiveTargetOffset = targetOffset;
@@ -3018,34 +2803,24 @@ const DrawerViewport = /*#__PURE__*/React__namespace.forwardRef(function DrawerV
3018
2803
  return closeFromSnapPoints();
3019
2804
  }
3020
2805
  }
3021
- const closeOffset = popupHeight;
3022
- const closeDistance = Math.abs(effectiveTargetOffset - closeOffset);
2806
+ const closeDistance = Math.abs(effectiveTargetOffset - popupHeight);
3023
2807
  const snapDistance = Math.abs(effectiveTargetOffset - targetSnapPoint.offset);
3024
2808
  if (closeDistance < snapDistance) {
3025
2809
  return closeFromSnapPoints();
3026
2810
  }
3027
- setActiveSnapPoint?.(targetSnapPoint.value, snapPointEventDetails);
2811
+ setActiveSnapPoint(targetSnapPoint.value, snapPointEventDetails);
3028
2812
  clearSwipeRelease();
3029
2813
  return false;
3030
2814
  }
3031
2815
  if (resolvedDirectionalVelocity >= FAST_SWIPE_VELOCITY && dragDelta > 0) {
3032
2816
  return closeFromSnapPoints();
3033
2817
  }
3034
- let closestSnapPoint = resolvedSnapPoints[0];
3035
- let closestDistance = Math.abs(targetOffset - closestSnapPoint.offset);
3036
- for (const snapPoint of resolvedSnapPoints) {
3037
- const distance = Math.abs(targetOffset - snapPoint.offset);
3038
- if (distance < closestDistance) {
3039
- closestDistance = distance;
3040
- closestSnapPoint = snapPoint;
3041
- }
3042
- }
3043
- const closeOffset = popupHeight;
3044
- const closeDistance = Math.abs(targetOffset - closeOffset);
3045
- if (closeDistance < closestDistance) {
2818
+ const closestSnapPoint = resolvedSnapPoints[closestSnapPointIndex(resolvedSnapPoints.map(point => point.offset), targetOffset)];
2819
+ const closeDistance = Math.abs(targetOffset - popupHeight);
2820
+ if (closeDistance < Math.abs(targetOffset - closestSnapPoint.offset)) {
3046
2821
  return closeFromSnapPoints();
3047
2822
  }
3048
- setActiveSnapPoint?.(closestSnapPoint.value, snapPointEventDetails);
2823
+ setActiveSnapPoint(closestSnapPoint.value, snapPointEventDetails);
3049
2824
  clearSwipeRelease();
3050
2825
  return false;
3051
2826
  },
@@ -3064,7 +2839,7 @@ const DrawerViewport = /*#__PURE__*/React__namespace.forwardRef(function DrawerV
3064
2839
  if (dismissEventDetails.isCanceled) {
3065
2840
  const pendingSnapPoint = pendingSwipeCloseSnapPointRef.current;
3066
2841
  if (pendingSnapPoint !== undefined) {
3067
- setActiveSnapPoint?.(pendingSnapPoint, useScrollLock.createChangeEventDetails(useScrollLock.swipe, event));
2842
+ setActiveSnapPoint(pendingSnapPoint, useScrollLock.createChangeEventDetails(useScrollLock.swipe, event));
3068
2843
  }
3069
2844
  pendingSwipeCloseSnapPointRef.current = undefined;
3070
2845
  resetSwipeRef.current?.();
@@ -3086,7 +2861,7 @@ const DrawerViewport = /*#__PURE__*/React__namespace.forwardRef(function DrawerV
3086
2861
  // Parent rejected: revert animation and restore snap point.
3087
2862
  const pendingSnapPoint = pendingSwipeCloseSnapPointRef.current;
3088
2863
  if (pendingSnapPoint !== undefined) {
3089
- setActiveSnapPoint?.(pendingSnapPoint, useScrollLock.createChangeEventDetails(useScrollLock.swipe, savedEvent));
2864
+ setActiveSnapPoint(pendingSnapPoint, useScrollLock.createChangeEventDetails(useScrollLock.swipe, savedEvent));
3090
2865
  }
3091
2866
  pendingSwipeCloseSnapPointRef.current = undefined;
3092
2867
  clearSwipeRelease();
@@ -3116,42 +2891,18 @@ const DrawerViewport = /*#__PURE__*/React__namespace.forwardRef(function DrawerV
3116
2891
  }
3117
2892
  const resolvedRootElement = rootElement;
3118
2893
  const doc = useScrollLock.ownerDocument(resolvedRootElement);
3119
- const win = floatingUi_utils.getWindow(doc);
3120
- function handleNativeTouchMove(event) {
3121
- // The virtual keyboard provider observes the move to tell a tap apart from a drag.
3122
- // It must run even when the swipe gesture below claims the event with
3123
- // `stopPropagation()`, which would otherwise prevent React's delegated handlers
3124
- // (and the provider) from ever seeing the move.
3125
- virtualKeyboard?.onTouchMove(event);
3126
- if (ignoreTouchSwipeRef.current) {
3127
- return;
3128
- }
3129
- const touchState = touchScrollStateRef.current;
3130
- const touch = event.touches[0];
3131
- if (!touch || !touchState) {
3132
- return;
3133
- }
2894
+ function processTouchMove(event, touchState, touch) {
3134
2895
  const drawerAxisDelta = isVerticalScrollAxis ? touch.clientY - touchState.lastY : touch.clientX - touchState.lastX;
3135
2896
 
3136
- // Preserve native range interaction by never locking touchmove for range inputs.
3137
- if (isEventOnRangeInput(event, win)) {
3138
- touchState.allowSwipe = false;
3139
- updateTouchScrollPosition(touchState, touch);
3140
- return;
3141
- }
3142
-
3143
2897
  // Avoid blocking pinch zoom or text selection adjustments on iOS Safari.
3144
2898
  if (event.touches.length === 2) {
3145
- updateTouchScrollPosition(touchState, touch);
3146
2899
  return;
3147
2900
  }
3148
2901
  const allowTouchMove = shouldIgnoreSwipeForTextSelection(doc, resolvedRootElement);
3149
2902
  if (allowTouchMove || !open || !mounted || nestedDrawerOpen) {
3150
- updateTouchScrollPosition(touchState, touch);
3151
2903
  return;
3152
2904
  }
3153
- if (preserveNativeCrossAxisScrollOnMove(touchState, touch, isVerticalScrollAxis)) {
3154
- updateTouchScrollPosition(touchState, touch);
2905
+ if (shouldYieldTouchMove(touchState, event, touch, isVerticalScrollAxis)) {
3155
2906
  return;
3156
2907
  }
3157
2908
  const scrollTarget = touchState.scrollTarget;
@@ -3163,27 +2914,21 @@ const DrawerViewport = /*#__PURE__*/React__namespace.forwardRef(function DrawerV
3163
2914
  // move through React re-rasterizes the popup content on every frame.
3164
2915
  event.stopPropagation();
3165
2916
  moveSwipeNative(event, resolvedRootElement);
3166
- updateTouchScrollPosition(touchState, touch);
3167
2917
  return;
3168
2918
  }
3169
- const hasScrollableContent = hasScrollableContentOnAxis(scrollTarget, scrollAxis);
3170
- if (!hasScrollableContent) {
2919
+ if (!hasScrollableContentOnAxis(scrollTarget, scrollAxis)) {
3171
2920
  // If the scroll container doesn't overflow on the drawer axis, prevent the window from
3172
2921
  // scrolling instead.
3173
2922
  if (event.cancelable) {
3174
2923
  event.preventDefault();
3175
2924
  }
3176
2925
  event.stopPropagation();
3177
- updateTouchScrollPosition(touchState, touch);
3178
2926
  return;
3179
2927
  }
3180
- const delta = drawerAxisDelta;
3181
- if (delta !== 0) {
3182
- const canSwipeFromScrollEdge = canSwipeFromScrollEdgeOnMove(scrollTarget, scrollAxis, swipeDirection, delta);
2928
+ if (drawerAxisDelta !== 0) {
2929
+ const canSwipeFromScrollEdge = canSwipeFromScrollEdgeOnMove(scrollTarget, scrollAxis, swipeDirection, drawerAxisDelta);
3183
2930
  if (!touchState.allowSwipe) {
3184
- if (!event.cancelable) {
3185
- touchState.allowSwipe = false;
3186
- } else if (canSwipeFromScrollEdge) {
2931
+ if (event.cancelable && canSwipeFromScrollEdge) {
3187
2932
  touchState.allowSwipe = true;
3188
2933
  event.preventDefault();
3189
2934
  } else {
@@ -3197,6 +2942,22 @@ const DrawerViewport = /*#__PURE__*/React__namespace.forwardRef(function DrawerV
3197
2942
  event.stopPropagation();
3198
2943
  moveSwipeNative(event, resolvedRootElement);
3199
2944
  }
2945
+ }
2946
+ function handleNativeTouchMove(event) {
2947
+ // The virtual keyboard provider observes the move to tell a tap apart from a drag.
2948
+ // It must run even when the swipe gesture below claims the event with
2949
+ // `stopPropagation()`, which would otherwise prevent React's delegated handlers
2950
+ // (and the provider) from ever seeing the move.
2951
+ virtualKeyboard?.onTouchMove(event);
2952
+ if (ignoreTouchSwipeRef.current) {
2953
+ return;
2954
+ }
2955
+ const touchState = touchScrollStateRef.current;
2956
+ const touch = event.touches[0];
2957
+ if (!touch || !touchState) {
2958
+ return;
2959
+ }
2960
+ processTouchMove(event, touchState, touch);
3200
2961
  updateTouchScrollPosition(touchState, touch);
3201
2962
  }
3202
2963
  return useScrollLock.addEventListener(doc, 'touchmove', handleNativeTouchMove, {
@@ -3204,18 +2965,13 @@ const DrawerViewport = /*#__PURE__*/React__namespace.forwardRef(function DrawerV
3204
2965
  capture: true
3205
2966
  });
3206
2967
  }, [mounted, nestedDrawerOpen, open, popupElementState, isVerticalScrollAxis, scrollAxis, swipeDirection, moveSwipeNative, viewportElement, virtualKeyboard]);
3207
- React__namespace.useEffect(() => {
2968
+ useScrollLock.useIsoLayoutEffect(() => {
3208
2969
  if (!snapPointRange || swipe.swiping) {
3209
2970
  return;
3210
2971
  }
3211
- const resolvedProgress = !open || nested ? 0 : snapPointProgress ?? 0;
3212
- applySwipeProgress({
3213
- resolvedProgress,
3214
- shouldTrackProgress: true,
3215
- notifyParent: false
3216
- });
2972
+ applySwipeProgress(!open || nested ? 0 : snapPointProgress ?? 0, true, false);
3217
2973
  }, [applySwipeProgress, frontmostHeight, nested, notifyParentSwipeProgressChange, open, snapPointProgress, snapPointRange, swipe.swiping, store, visualStateStore]);
3218
- React__namespace.useEffect(() => {
2974
+ useScrollLock.useIsoLayoutEffect(() => {
3219
2975
  if (!notifyParentSwipeProgressChange) {
3220
2976
  return undefined;
3221
2977
  }
@@ -3226,13 +2982,19 @@ const DrawerViewport = /*#__PURE__*/React__namespace.forwardRef(function DrawerV
3226
2982
  notifyParentSwipeProgressChange(0);
3227
2983
  };
3228
2984
  }, [notifyParentSwipeProgressChange, open]);
3229
- React__namespace.useEffect(() => {
2985
+ useScrollLock.useIsoLayoutEffect(() => {
3230
2986
  if (open) {
3231
- resetSwipe();
2987
+ // Skip `resetSwipe` while `Drawer.SwipeArea` is driving the open: it zeroes the popup's
2988
+ // `--swipe-movement-*` (via `syncDragStyles(false)`), flashing it fully open for a frame.
2989
+ // `clearSwipeRelease` doesn't touch those vars, so always run it to clear any leftover
2990
+ // release state from a prior dismiss (e.g. when the popup is kept mounted).
2991
+ if (!swipeAreaActiveRef.current) {
2992
+ resetSwipe();
2993
+ }
3232
2994
  clearSwipeRelease();
3233
2995
  }
3234
- }, [clearSwipeRelease, open, resetSwipe]);
3235
- React__namespace.useEffect(() => {
2996
+ }, [clearSwipeRelease, open, resetSwipe, swipeAreaActiveRef]);
2997
+ useScrollLock.useIsoLayoutEffect(() => {
3236
2998
  const backdropElement = backdropRef.current;
3237
2999
  return () => {
3238
3000
  visualStateStore?.set({
@@ -3257,12 +3019,19 @@ const DrawerViewport = /*#__PURE__*/React__namespace.forwardRef(function DrawerV
3257
3019
  swipeStrength: swipeRelease ?? null,
3258
3020
  setSwipeDismissed
3259
3021
  }), [setSwipeDismissed, swipe.getDragStyles, swipe.swiping, swipeRelease]);
3260
- function resetTouchTrackingState() {
3261
- ignoreTouchSwipeRef.current = false;
3022
+ function resetTouchSwipeState(ignoreSwipe) {
3023
+ ignoreTouchSwipeRef.current = ignoreSwipe;
3262
3024
  touchScrollStateRef.current = null;
3025
+ }
3026
+ function resetTouchTrackingState() {
3027
+ resetTouchSwipeState(false);
3263
3028
  lastPointerTypeRef.current = '';
3264
3029
  ignoreNextTouchStartFromPenRef.current = false;
3265
3030
  }
3031
+ function handlePointerEnd(event) {
3032
+ lastPointerTypeRef.current = '';
3033
+ return event.pointerType !== 'touch';
3034
+ }
3266
3035
  return /*#__PURE__*/jsxRuntime.jsx(DialogViewport, {
3267
3036
  ref: forwardedRef,
3268
3037
  className: className,
@@ -3275,8 +3044,7 @@ const DrawerViewport = /*#__PURE__*/React__namespace.forwardRef(function DrawerV
3275
3044
  if (!open || !mounted || nestedDrawerOpen) {
3276
3045
  return;
3277
3046
  }
3278
- const doc = useScrollLock.ownerDocument(event.currentTarget);
3279
- const elementAtPoint = getElementAtPoint(doc, event.clientX, event.clientY);
3047
+ const elementAtPoint = getElementAtPoint(event.currentTarget.getRootNode(), event.clientX, event.clientY);
3280
3048
  if (isSwipeIgnoredTarget(elementAtPoint) || isDrawerContentTarget(elementAtPoint)) {
3281
3049
  return;
3282
3050
  }
@@ -3292,34 +3060,24 @@ const DrawerViewport = /*#__PURE__*/React__namespace.forwardRef(function DrawerV
3292
3060
  swipePointerProps.onPointerMove?.(event);
3293
3061
  },
3294
3062
  onPointerUp(event) {
3295
- if (lastPointerTypeRef.current === event.pointerType) {
3296
- lastPointerTypeRef.current = '';
3297
- }
3298
- if (event.pointerType === 'touch') {
3299
- return;
3063
+ if (handlePointerEnd(event)) {
3064
+ swipePointerProps.onPointerUp?.(event);
3300
3065
  }
3301
- swipePointerProps.onPointerUp?.(event);
3302
3066
  },
3303
3067
  onPointerCancel(event) {
3304
- if (lastPointerTypeRef.current === event.pointerType) {
3305
- lastPointerTypeRef.current = '';
3306
- }
3307
- if (event.pointerType === 'touch') {
3308
- return;
3068
+ if (handlePointerEnd(event)) {
3069
+ swipePointerProps.onPointerCancel?.(event);
3309
3070
  }
3310
- swipePointerProps.onPointerCancel?.(event);
3311
3071
  },
3312
3072
  onTouchStart(event) {
3313
3073
  const startedFromPenPointerDown = lastPointerTypeRef.current === 'pen' && ignoreNextTouchStartFromPenRef.current;
3314
3074
  if (startedFromPenPointerDown) {
3315
3075
  ignoreNextTouchStartFromPenRef.current = false;
3316
- ignoreTouchSwipeRef.current = false;
3317
- touchScrollStateRef.current = null;
3076
+ resetTouchSwipeState(false);
3318
3077
  return;
3319
3078
  }
3320
3079
  if (!open || !mounted || nestedDrawerOpen) {
3321
- ignoreTouchSwipeRef.current = false;
3322
- touchScrollStateRef.current = null;
3080
+ resetTouchSwipeState(false);
3323
3081
  return;
3324
3082
  }
3325
3083
  const touch = event.touches[0];
@@ -3327,32 +3085,25 @@ const DrawerViewport = /*#__PURE__*/React__namespace.forwardRef(function DrawerV
3327
3085
  return;
3328
3086
  }
3329
3087
  if (isReactTouchEventOnRangeInput(event)) {
3330
- ignoreTouchSwipeRef.current = false;
3331
- touchScrollStateRef.current = null;
3088
+ resetTouchSwipeState(false);
3332
3089
  return;
3333
3090
  }
3334
- const doc = useScrollLock.ownerDocument(event.currentTarget);
3335
- const elementAtPoint = getElementAtPoint(doc, touch.clientX, touch.clientY);
3336
- const rootElement = viewportElement ?? popupElementState;
3091
+ const rootElement = event.currentTarget;
3092
+ const elementAtPoint = getElementAtPoint(rootElement.getRootNode(), touch.clientX, touch.clientY);
3337
3093
  const eventTarget = useScrollLock.getTarget(event.nativeEvent);
3338
- const target = floatingUi_utils.isElement(eventTarget) ? eventTarget : null;
3339
- if (rootElement && target && !useScrollLock.contains(rootElement, target)) {
3340
- ignoreTouchSwipeRef.current = true;
3341
- touchScrollStateRef.current = null;
3094
+ const target = useScrollLock.isElement(eventTarget) ? eventTarget : rootElement;
3095
+ if (!useScrollLock.contains(rootElement, target)) {
3096
+ resetTouchSwipeState(true);
3342
3097
  return;
3343
3098
  }
3344
3099
  virtualKeyboard?.onTouchStart(event);
3345
- ignoreTouchSwipeRef.current = isSwipeIgnoredTarget(elementAtPoint);
3346
- if (ignoreTouchSwipeRef.current) {
3347
- touchScrollStateRef.current = null;
3100
+ if (isSwipeIgnoredTarget(elementAtPoint)) {
3101
+ resetTouchSwipeState(true);
3348
3102
  return;
3349
3103
  }
3350
- let scrollTarget = null;
3351
- let hasCrossAxisScrollableContent = false;
3352
- if (rootElement && target) {
3353
- scrollTarget = findScrollableTouchTarget(target, rootElement, scrollAxis);
3354
- hasCrossAxisScrollableContent = findScrollableTouchTarget(target, rootElement, crossScrollAxis) != null;
3355
- }
3104
+ ignoreTouchSwipeRef.current = false;
3105
+ const scrollTarget = scrollable.findScrollableTouchTarget(target, rootElement, scrollAxis);
3106
+ const hasCrossAxisScrollableContent = scrollable.findScrollableTouchTarget(target, rootElement, crossScrollAxis) != null;
3356
3107
  let allowSwipe = null;
3357
3108
  if (scrollTarget) {
3358
3109
  const canSwipeFromEdge = isAtSwipeStartEdge(scrollTarget, scrollAxis, swipeDirection);
@@ -3366,7 +3117,8 @@ const DrawerViewport = /*#__PURE__*/React__namespace.forwardRef(function DrawerV
3366
3117
  scrollTarget,
3367
3118
  hasCrossAxisScrollableContent,
3368
3119
  allowSwipe,
3369
- preserveNativeCrossAxisScroll: false
3120
+ preserveNativeCrossAxisScroll: false,
3121
+ drawerAxisAttributed: false
3370
3122
  };
3371
3123
  swipeTouchProps.onTouchStart?.(event);
3372
3124
  },
@@ -3391,24 +3143,8 @@ const DrawerViewport = /*#__PURE__*/React__namespace.forwardRef(function DrawerV
3391
3143
  });
3392
3144
  });
3393
3145
  if (process.env.NODE_ENV !== "production") DrawerViewport.displayName = "DrawerViewport";
3394
- function setSwipeDismissedElements(popupElement, backdropElement, dismissed) {
3395
- if (dismissed) {
3396
- popupElement?.setAttribute(DrawerPopupDataAttributes.swipeDismiss, '');
3397
- backdropElement?.setAttribute(DrawerPopupDataAttributes.swipeDismiss, '');
3398
- return;
3399
- }
3400
- popupElement?.removeAttribute(DrawerPopupDataAttributes.swipeDismiss);
3401
- backdropElement?.removeAttribute(DrawerPopupDataAttributes.swipeDismiss);
3402
- }
3403
3146
  function setBackdropSwipingAttribute(backdropElement, swiping) {
3404
- if (!backdropElement) {
3405
- return;
3406
- }
3407
- if (swiping) {
3408
- backdropElement.setAttribute(DrawerPopupDataAttributes.swiping, '');
3409
- return;
3410
- }
3411
- backdropElement.removeAttribute(DrawerPopupDataAttributes.swiping);
3147
+ backdropElement?.toggleAttribute(DrawerPopupDataAttributes.swiping, swiping);
3412
3148
  }
3413
3149
  function isSwipeIgnoredTarget(target) {
3414
3150
  return Boolean(target?.closest(useScrollLock.BASE_UI_SWIPE_IGNORE_SELECTOR));
@@ -3416,28 +3152,26 @@ function isSwipeIgnoredTarget(target) {
3416
3152
  function isDrawerContentTarget(target) {
3417
3153
  return Boolean(target?.closest(DRAWER_CONTENT_SELECTOR));
3418
3154
  }
3155
+ function getBaseSwipeSize(element, direction) {
3156
+ return direction === 'left' || direction === 'right' ? element.offsetWidth : element.offsetHeight;
3157
+ }
3419
3158
  function getBaseSwipeThreshold(element, direction) {
3420
- const size = direction === 'left' || direction === 'right' ? element.offsetWidth : element.offsetHeight;
3421
- return Math.max(size * 0.5, MIN_SWIPE_THRESHOLD);
3159
+ return Math.max(getBaseSwipeSize(element, direction) * 0.5, MIN_SWIPE_THRESHOLD);
3422
3160
  }
3423
3161
  function isRangeInput(target, win) {
3424
3162
  return target instanceof win.HTMLInputElement && target.type === 'range';
3425
3163
  }
3426
3164
  function isTextSelectionControl(target) {
3427
- if (!floatingUi_utils.isElement(target)) {
3428
- return false;
3429
- }
3430
3165
  return target.tagName === 'INPUT' || target.tagName === 'TEXTAREA';
3431
3166
  }
3432
3167
  function hasExpandedSelectionWithinTarget(selection, target) {
3433
- const anchorElement = floatingUi_utils.isElement(selection.anchorNode) ? selection.anchorNode : selection.anchorNode?.parentElement;
3434
- const focusElement = floatingUi_utils.isElement(selection.focusNode) ? selection.focusNode : selection.focusNode?.parentElement;
3168
+ const anchorElement = useScrollLock.isElement(selection.anchorNode) ? selection.anchorNode : selection.anchorNode?.parentElement;
3169
+ const focusElement = useScrollLock.isElement(selection.focusNode) ? selection.focusNode : selection.focusNode?.parentElement;
3435
3170
  return selection.containsNode(target, true) || useScrollLock.contains(target, anchorElement) || useScrollLock.contains(target, focusElement);
3436
3171
  }
3437
3172
  function shouldIgnoreSwipeForTextSelection(doc, rootElement) {
3438
3173
  const activeEl = useScrollLock.activeElement(doc);
3439
- const activeElementWithinRoot = Boolean(activeEl && useScrollLock.contains(rootElement, activeEl));
3440
- if (activeElementWithinRoot && isTextSelectionControl(activeEl)) {
3174
+ if (activeEl && useScrollLock.contains(rootElement, activeEl) && isTextSelectionControl(activeEl)) {
3441
3175
  const {
3442
3176
  selectionStart,
3443
3177
  selectionEnd
@@ -3453,38 +3187,60 @@ function shouldIgnoreSwipeForTextSelection(doc, rootElement) {
3453
3187
  return hasExpandedSelectionWithinTarget(selection, rootElement);
3454
3188
  }
3455
3189
  function isEventOnRangeInput(event, win) {
3456
- const composedPath = event.composedPath();
3457
- if (composedPath) {
3458
- return composedPath.some(pathTarget => isRangeInput(pathTarget, win));
3459
- }
3460
- return isRangeInput(useScrollLock.getTarget(event), win);
3190
+ return event.composedPath().some(pathTarget => isRangeInput(pathTarget, win));
3461
3191
  }
3462
3192
  function isReactTouchEventOnRangeInput(event) {
3463
- return isEventOnRangeInput(event.nativeEvent, floatingUi_utils.getWindow(event.currentTarget));
3193
+ return isEventOnRangeInput(event.nativeEvent, useScrollLock.getWindow(event.currentTarget));
3464
3194
  }
3465
3195
  function updateTouchScrollPosition(touchState, touch) {
3466
3196
  touchState.lastX = touch.clientX;
3467
3197
  touchState.lastY = touch.clientY;
3468
3198
  }
3469
- function preserveNativeCrossAxisScrollOnMove(touchState, touch, isVerticalScrollAxis) {
3199
+
3200
+ /**
3201
+ * Arbitrates a touchmove between the drawer swipe and a native cross-axis scroll.
3202
+ * Returns `true` when the move must be left alone — either because the cross axis already won the
3203
+ * gesture, or because neither axis has passed the slop yet and the gesture cannot be attributed.
3204
+ */
3205
+ function shouldYieldTouchMove(touchState, event, touch, isVerticalScrollAxis) {
3470
3206
  if (touchState.preserveNativeCrossAxisScroll) {
3471
3207
  return true;
3472
3208
  }
3473
- if (touchState.allowSwipe === true || !touchState.hasCrossAxisScrollableContent) {
3209
+
3210
+ // Attribution happens once per gesture. Re-arbitrating after the drawer axis has won would let
3211
+ // the pre-attribution branches below fire mid-drag (the slop is measured from the touch origin,
3212
+ // which is never re-baselined), freezing the popup and dropping `preventDefault()`.
3213
+ if (touchState.drawerAxisAttributed || touchState.allowSwipe === true || !touchState.hasCrossAxisScrollableContent) {
3474
3214
  return false;
3475
3215
  }
3216
+
3217
+ // A non-cancelable touchmove means the browser has already committed the gesture to a native
3218
+ // scroll; claiming it for the swipe would drag the popup alongside the scrolling content.
3219
+ if (!event.cancelable) {
3220
+ touchState.preserveNativeCrossAxisScroll = true;
3221
+ return true;
3222
+ }
3476
3223
  const drawerAxisGestureDelta = isVerticalScrollAxis ? touch.clientY - touchState.startY : touch.clientX - touchState.startX;
3477
3224
  const crossAxisGestureDelta = isVerticalScrollAxis ? touch.clientX - touchState.startX : touch.clientY - touchState.startY;
3478
3225
  const absDrawerAxisGestureDelta = Math.abs(drawerAxisGestureDelta);
3479
3226
  const absCrossAxisGestureDelta = Math.abs(crossAxisGestureDelta);
3480
- if (absCrossAxisGestureDelta < 6 || absCrossAxisGestureDelta <= absDrawerAxisGestureDelta + 2) {
3227
+ if (absCrossAxisGestureDelta >= AXIS_LOCK_SLOP && absCrossAxisGestureDelta > absDrawerAxisGestureDelta + AXIS_LOCK_BIAS) {
3228
+ touchState.preserveNativeCrossAxisScroll = true;
3229
+ return true;
3230
+ }
3231
+ if (absDrawerAxisGestureDelta >= AXIS_LOCK_SLOP) {
3232
+ touchState.drawerAxisAttributed = true;
3481
3233
  return false;
3482
3234
  }
3483
- touchState.preserveNativeCrossAxisScroll = true;
3235
+
3236
+ // Neither axis has traveled past the slop yet, so the gesture cannot be attributed. Leave the
3237
+ // event alone: on iOS, `preventDefault()` on the first cancelable touchmove cancels native
3238
+ // scrolling for the entire gesture, which would lock a cross-axis scroll that only passes the
3239
+ // slop on a later move.
3484
3240
  return true;
3485
3241
  }
3486
3242
  function hasScrollableContentOnAxis(scrollTarget, axis) {
3487
- return axis === 'vertical' ? scrollTarget.scrollHeight > scrollTarget.clientHeight : scrollTarget.scrollWidth > scrollTarget.clientWidth;
3243
+ return getScrollMetrics(scrollTarget, axis).max > 0;
3488
3244
  }
3489
3245
  function getScrollMetrics(scrollTarget, axis) {
3490
3246
  if (axis === 'vertical') {
@@ -3501,48 +3257,23 @@ function getScrollMetrics(scrollTarget, axis) {
3501
3257
  };
3502
3258
  }
3503
3259
  function isAtSwipeStartEdge(scrollTarget, axis, direction) {
3260
+ const dismissFromStartEdge = shouldDismissFromStartEdge(direction, axis);
3504
3261
  const {
3505
3262
  offset,
3506
3263
  max
3507
3264
  } = getScrollMetrics(scrollTarget, axis);
3508
- const dismissFromStartEdge = shouldDismissFromStartEdge(direction, axis);
3509
- if (dismissFromStartEdge === null) {
3510
- return false;
3511
- }
3512
3265
  return dismissFromStartEdge ? offset <= 0 : offset >= max;
3513
3266
  }
3514
3267
  function canSwipeFromScrollEdgeOnMove(scrollTarget, axis, direction, delta) {
3515
- const {
3516
- offset,
3517
- max
3518
- } = getScrollMetrics(scrollTarget, axis);
3519
3268
  const dismissFromStartEdge = shouldDismissFromStartEdge(direction, axis);
3520
- if (dismissFromStartEdge === null) {
3521
- return false;
3522
- }
3523
3269
  const movingTowardDismiss = dismissFromStartEdge ? delta > 0 : delta < 0;
3524
3270
  if (!movingTowardDismiss) {
3525
3271
  return false;
3526
3272
  }
3527
- return dismissFromStartEdge ? offset <= 0 : offset >= max;
3273
+ return isAtSwipeStartEdge(scrollTarget, axis, direction);
3528
3274
  }
3529
3275
  function shouldDismissFromStartEdge(direction, axis) {
3530
- if (axis === 'vertical') {
3531
- if (direction === 'down') {
3532
- return true;
3533
- }
3534
- if (direction === 'up') {
3535
- return false;
3536
- }
3537
- return null;
3538
- }
3539
- if (direction === 'right') {
3540
- return true;
3541
- }
3542
- if (direction === 'left') {
3543
- return false;
3544
- }
3545
- return null;
3276
+ return axis === 'vertical' ? direction === 'down' : direction === 'right';
3546
3277
  }
3547
3278
 
3548
3279
  const ScrollAreaRootContext = /*#__PURE__*/React__namespace.createContext(undefined);
@@ -3555,20 +3286,6 @@ function useScrollAreaRootContext() {
3555
3286
  return context;
3556
3287
  }
3557
3288
 
3558
- let ScrollAreaRootCssVars = /*#__PURE__*/function (ScrollAreaRootCssVars) {
3559
- /**
3560
- * The scroll area's corner height.
3561
- * @type {number}
3562
- */
3563
- ScrollAreaRootCssVars["scrollAreaCornerHeight"] = "--scroll-area-corner-height";
3564
- /**
3565
- * The scroll area's corner width.
3566
- * @type {number}
3567
- */
3568
- ScrollAreaRootCssVars["scrollAreaCornerWidth"] = "--scroll-area-corner-width";
3569
- return ScrollAreaRootCssVars;
3570
- }({});
3571
-
3572
3289
  const SCROLL_TIMEOUT = 500;
3573
3290
  const MIN_THUMB_SIZE = 16;
3574
3291
 
@@ -3577,108 +3294,28 @@ function getOffset(element, prop, axis) {
3577
3294
  return 0;
3578
3295
  }
3579
3296
  const styles = getComputedStyle(element);
3580
- const propAxis = axis === 'x' ? 'Inline' : 'Block';
3297
+ const key = `${prop}${axis === 'x' ? 'Inline' : 'Block'}`;
3298
+ const start = parseFloat(styles[`${key}Start`]);
3581
3299
 
3582
3300
  // Safari misreports `marginInlineEnd` in RTL.
3583
3301
  // We have to assume the start/end values are symmetrical, which is likely.
3584
3302
  if (axis === 'x' && prop === 'margin') {
3585
- return parseFloat(styles[`${prop}InlineStart`]) * 2;
3303
+ return start * 2;
3586
3304
  }
3587
- return parseFloat(styles[`${prop}${propAxis}Start`]) + parseFloat(styles[`${prop}${propAxis}End`]);
3305
+ return start + parseFloat(styles[`${key}End`]);
3588
3306
  }
3589
3307
 
3590
- let ScrollAreaScrollbarDataAttributes = /*#__PURE__*/function (ScrollAreaScrollbarDataAttributes) {
3591
- /**
3592
- * Indicates the orientation of the scrollbar.
3593
- * @type {'horizontal' | 'vertical'}
3594
- */
3595
- ScrollAreaScrollbarDataAttributes["orientation"] = "data-orientation";
3596
- /**
3597
- * Present when the pointer is over the scroll area.
3598
- */
3599
- ScrollAreaScrollbarDataAttributes["hovering"] = "data-hovering";
3600
- /**
3601
- * Present when the user scrolls inside the scroll area.
3602
- */
3603
- ScrollAreaScrollbarDataAttributes["scrolling"] = "data-scrolling";
3604
- /**
3605
- * Present when the scroll area content is wider than the viewport.
3606
- */
3607
- ScrollAreaScrollbarDataAttributes["hasOverflowX"] = "data-has-overflow-x";
3608
- /**
3609
- * Present when the scroll area content is taller than the viewport.
3610
- */
3611
- ScrollAreaScrollbarDataAttributes["hasOverflowY"] = "data-has-overflow-y";
3612
- /**
3613
- * Present when there is overflow on the horizontal start side.
3614
- */
3615
- ScrollAreaScrollbarDataAttributes["overflowXStart"] = "data-overflow-x-start";
3616
- /**
3617
- * Present when there is overflow on the horizontal end side.
3618
- */
3619
- ScrollAreaScrollbarDataAttributes["overflowXEnd"] = "data-overflow-x-end";
3620
- /**
3621
- * Present when there is overflow on the vertical start side.
3622
- */
3623
- ScrollAreaScrollbarDataAttributes["overflowYStart"] = "data-overflow-y-start";
3624
- /**
3625
- * Present when there is overflow on the vertical end side.
3626
- */
3627
- ScrollAreaScrollbarDataAttributes["overflowYEnd"] = "data-overflow-y-end";
3628
- return ScrollAreaScrollbarDataAttributes;
3629
- }({});
3630
-
3631
- let ScrollAreaRootDataAttributes = /*#__PURE__*/function (ScrollAreaRootDataAttributes) {
3632
- /**
3633
- * Present when the user scrolls inside the scroll area.
3634
- */
3635
- ScrollAreaRootDataAttributes["scrolling"] = "data-scrolling";
3636
- /**
3637
- * Present when the scroll area content is wider than the viewport.
3638
- */
3639
- ScrollAreaRootDataAttributes["hasOverflowX"] = "data-has-overflow-x";
3640
- /**
3641
- * Present when the scroll area content is taller than the viewport.
3642
- */
3643
- ScrollAreaRootDataAttributes["hasOverflowY"] = "data-has-overflow-y";
3644
- /**
3645
- * Present when there is overflow on the horizontal start side.
3646
- */
3647
- ScrollAreaRootDataAttributes["overflowXStart"] = "data-overflow-x-start";
3648
- /**
3649
- * Present when there is overflow on the horizontal end side.
3650
- */
3651
- ScrollAreaRootDataAttributes["overflowXEnd"] = "data-overflow-x-end";
3652
- /**
3653
- * Present when there is overflow on the vertical start side.
3654
- */
3655
- ScrollAreaRootDataAttributes["overflowYStart"] = "data-overflow-y-start";
3656
- /**
3657
- * Present when there is overflow on the vertical end side.
3658
- */
3659
- ScrollAreaRootDataAttributes["overflowYEnd"] = "data-overflow-y-end";
3660
- return ScrollAreaRootDataAttributes;
3661
- }({});
3662
-
3308
+ // Data-attribute strings inlined so `ScrollAreaRootDataAttributes` tree-shakes out.
3309
+ const attr = name => value => value ? {
3310
+ [name]: ''
3311
+ } : null;
3663
3312
  const scrollAreaStateAttributesMapping = {
3664
- hasOverflowX: value => value ? {
3665
- [ScrollAreaRootDataAttributes.hasOverflowX]: ''
3666
- } : null,
3667
- hasOverflowY: value => value ? {
3668
- [ScrollAreaRootDataAttributes.hasOverflowY]: ''
3669
- } : null,
3670
- overflowXStart: value => value ? {
3671
- [ScrollAreaRootDataAttributes.overflowXStart]: ''
3672
- } : null,
3673
- overflowXEnd: value => value ? {
3674
- [ScrollAreaRootDataAttributes.overflowXEnd]: ''
3675
- } : null,
3676
- overflowYStart: value => value ? {
3677
- [ScrollAreaRootDataAttributes.overflowYStart]: ''
3678
- } : null,
3679
- overflowYEnd: value => value ? {
3680
- [ScrollAreaRootDataAttributes.overflowYEnd]: ''
3681
- } : null,
3313
+ hasOverflowX: attr('data-has-overflow-x'),
3314
+ hasOverflowY: attr('data-has-overflow-y'),
3315
+ overflowXStart: attr('data-overflow-x-start'),
3316
+ overflowXEnd: attr('data-overflow-x-end'),
3317
+ overflowYStart: attr('data-overflow-y-start'),
3318
+ overflowYEnd: attr('data-overflow-y-end'),
3682
3319
  cornerHidden: () => null
3683
3320
  };
3684
3321
 
@@ -3744,99 +3381,145 @@ const ScrollAreaRoot = /*#__PURE__*/React__namespace.forwardRef(function ScrollA
3744
3381
  const thumbYRef = React__namespace.useRef(null);
3745
3382
  const thumbXRef = React__namespace.useRef(null);
3746
3383
  const cornerRef = React__namespace.useRef(null);
3747
- const thumbDraggingRef = React__namespace.useRef(false);
3384
+ const activePointerIdRef = React__namespace.useRef(null);
3748
3385
  const startYRef = React__namespace.useRef(0);
3749
3386
  const startXRef = React__namespace.useRef(0);
3750
3387
  const startScrollTopRef = React__namespace.useRef(0);
3751
3388
  const startScrollLeftRef = React__namespace.useRef(0);
3752
3389
  const currentOrientationRef = React__namespace.useRef('vertical');
3753
3390
  const scrollPositionRef = React__namespace.useRef(DEFAULT_COORDS);
3391
+ const savedSnapTypeRef = React__namespace.useRef(null);
3392
+ function startScrolling(vertical) {
3393
+ const setScrolling = vertical ? setScrollingY : setScrollingX;
3394
+ const timeout = vertical ? scrollYTimeout : scrollXTimeout;
3395
+ setScrolling(true);
3396
+ timeout.start(SCROLL_TIMEOUT, () => {
3397
+ setScrolling(false);
3398
+ });
3399
+ }
3754
3400
  const handleScroll = useScrollLock.useStableCallback(scrollPosition => {
3755
3401
  const offsetX = scrollPosition.x - scrollPositionRef.current.x;
3756
3402
  const offsetY = scrollPosition.y - scrollPositionRef.current.y;
3757
3403
  scrollPositionRef.current = scrollPosition;
3758
3404
  if (offsetY !== 0) {
3759
- setScrollingY(true);
3760
- scrollYTimeout.start(SCROLL_TIMEOUT, () => {
3761
- setScrollingY(false);
3762
- });
3405
+ startScrolling(true);
3763
3406
  }
3764
3407
  if (offsetX !== 0) {
3765
- setScrollingX(true);
3766
- scrollXTimeout.start(SCROLL_TIMEOUT, () => {
3767
- setScrollingX(false);
3768
- });
3408
+ startScrolling(false);
3409
+ }
3410
+ });
3411
+
3412
+ // CSS scroll snap forces every programmatic scroll to land on a snap
3413
+ // point, making thumb dragging jump between snap points. Native
3414
+ // scrollbars suppress snapping while dragging, so disable it until the
3415
+ // pointer is released; restoring the value re-snaps the viewport. The
3416
+ // save is guarded so a second pointer during an active drag can't
3417
+ // clobber the saved value with `none`.
3418
+ const disableViewportSnap = useScrollLock.useStableCallback(() => {
3419
+ const viewportEl = viewportRef.current;
3420
+ if (viewportEl && savedSnapTypeRef.current === null) {
3421
+ savedSnapTypeRef.current = viewportEl.style.scrollSnapType;
3422
+ viewportEl.style.scrollSnapType = 'none';
3769
3423
  }
3770
3424
  });
3771
3425
  const handlePointerDown = useScrollLock.useStableCallback(event => {
3772
3426
  if (event.button !== 0) {
3773
3427
  return;
3774
3428
  }
3775
- thumbDraggingRef.current = true;
3429
+ if (activePointerIdRef.current !== null) {
3430
+ const activeThumb = currentOrientationRef.current === 'vertical' ? thumbYRef.current : thumbXRef.current;
3431
+ // A live drag holds capture for the active pointer — ignore other pointers.
3432
+ // No capture means the release went missing entirely (silent capture drop
3433
+ // with an id that never reappears, e.g. a lost touch contact), so let the
3434
+ // new pointer take over the latch instead of leaving dragging dead.
3435
+ if (activeThumb?.hasPointerCapture(activePointerIdRef.current)) {
3436
+ return;
3437
+ }
3438
+ }
3439
+ activePointerIdRef.current = event.pointerId;
3776
3440
  startYRef.current = event.clientY;
3777
3441
  startXRef.current = event.clientX;
3778
- currentOrientationRef.current = event.currentTarget.getAttribute(ScrollAreaScrollbarDataAttributes.orientation);
3779
- if (viewportRef.current) {
3780
- startScrollTopRef.current = viewportRef.current.scrollTop;
3781
- startScrollLeftRef.current = viewportRef.current.scrollLeft;
3782
- }
3783
- if (thumbYRef.current && currentOrientationRef.current === 'vertical') {
3784
- thumbYRef.current.setPointerCapture(event.pointerId);
3785
- }
3786
- if (thumbXRef.current && currentOrientationRef.current === 'horizontal') {
3787
- thumbXRef.current.setPointerCapture(event.pointerId);
3442
+ // Literal instead of `ScrollAreaScrollbarDataAttributes.orientation`: referencing an
3443
+ // enum member retains its whole object in the bundle, so the strings are inlined and
3444
+ // the enums kept for docs only.
3445
+ currentOrientationRef.current = event.currentTarget.getAttribute('data-orientation');
3446
+ const viewportEl = viewportRef.current;
3447
+ if (viewportEl) {
3448
+ startScrollTopRef.current = viewportEl.scrollTop;
3449
+ startScrollLeftRef.current = viewportEl.scrollLeft;
3450
+ disableViewportSnap();
3788
3451
  }
3452
+ const thumb = currentOrientationRef.current === 'vertical' ? thumbYRef.current : thumbXRef.current;
3453
+ thumb?.setPointerCapture(event.pointerId);
3789
3454
  });
3790
- const handlePointerMove = useScrollLock.useStableCallback(event => {
3791
- if (!thumbDraggingRef.current) {
3455
+ const handlePointerUp = useScrollLock.useStableCallback(event => {
3456
+ if (event.pointerId !== activePointerIdRef.current) {
3792
3457
  return;
3793
3458
  }
3794
- const deltaY = event.clientY - startYRef.current;
3795
- const deltaX = event.clientX - startXRef.current;
3796
- if (viewportRef.current) {
3797
- const scrollableContentHeight = viewportRef.current.scrollHeight;
3798
- const viewportHeight = viewportRef.current.clientHeight;
3799
- const scrollableContentWidth = viewportRef.current.scrollWidth;
3800
- const viewportWidth = viewportRef.current.clientWidth;
3801
- if (thumbYRef.current && scrollbarYRef.current && currentOrientationRef.current === 'vertical') {
3802
- const scrollbarYOffset = getOffset(scrollbarYRef.current, 'padding', 'y');
3803
- const thumbYOffset = getOffset(thumbYRef.current, 'margin', 'y');
3804
- const thumbHeight = thumbYRef.current.offsetHeight;
3805
- const maxThumbOffsetY = scrollbarYRef.current.offsetHeight - thumbHeight - scrollbarYOffset - thumbYOffset;
3806
- const scrollRatioY = deltaY / maxThumbOffsetY;
3807
- viewportRef.current.scrollTop = startScrollTopRef.current + scrollRatioY * (scrollableContentHeight - viewportHeight);
3808
- event.preventDefault();
3809
- setScrollingY(true);
3810
- scrollYTimeout.start(SCROLL_TIMEOUT, () => {
3811
- setScrollingY(false);
3812
- });
3813
- }
3814
- if (thumbXRef.current && scrollbarXRef.current && currentOrientationRef.current === 'horizontal') {
3815
- const scrollbarXOffset = getOffset(scrollbarXRef.current, 'padding', 'x');
3816
- const thumbXOffset = getOffset(thumbXRef.current, 'margin', 'x');
3817
- const thumbWidth = thumbXRef.current.offsetWidth;
3818
- const maxThumbOffsetX = scrollbarXRef.current.offsetWidth - thumbWidth - scrollbarXOffset - thumbXOffset;
3819
- const scrollRatioX = deltaX / maxThumbOffsetX;
3820
- viewportRef.current.scrollLeft = startScrollLeftRef.current + scrollRatioX * (scrollableContentWidth - viewportWidth);
3821
- event.preventDefault();
3822
- setScrollingX(true);
3823
- scrollXTimeout.start(SCROLL_TIMEOUT, () => {
3824
- setScrollingX(false);
3825
- });
3459
+ activePointerIdRef.current = null;
3460
+ // Clear the drag's scrolling state immediately rather than waiting for the
3461
+ // `SCROLL_TIMEOUT` timer armed by the last drag move, so every release path
3462
+ // (real, `pointercancel`, or the missed-release fallback) behaves the same.
3463
+ (currentOrientationRef.current === 'vertical' ? setScrollingY : setScrollingX)(false);
3464
+ if (savedSnapTypeRef.current !== null) {
3465
+ if (viewportRef.current) {
3466
+ viewportRef.current.style.scrollSnapType = savedSnapTypeRef.current;
3826
3467
  }
3468
+ savedSnapTypeRef.current = null;
3827
3469
  }
3828
- });
3829
- const handlePointerUp = useScrollLock.useStableCallback(event => {
3830
- thumbDraggingRef.current = false;
3831
-
3470
+ const thumb = currentOrientationRef.current === 'vertical' ? thumbYRef.current : thumbXRef.current;
3832
3471
  // `pointercancel` releases capture implicitly, so guard against releasing a
3833
3472
  // capture we no longer hold (which would throw).
3834
- if (thumbYRef.current && currentOrientationRef.current === 'vertical' && thumbYRef.current.hasPointerCapture(event.pointerId)) {
3835
- thumbYRef.current.releasePointerCapture(event.pointerId);
3473
+ if (thumb?.hasPointerCapture(event.pointerId)) {
3474
+ thumb.releasePointerCapture(event.pointerId);
3836
3475
  }
3837
- if (thumbXRef.current && currentOrientationRef.current === 'horizontal' && thumbXRef.current.hasPointerCapture(event.pointerId)) {
3838
- thumbXRef.current.releasePointerCapture(event.pointerId);
3476
+ });
3477
+ const handlePointerMove = useScrollLock.useStableCallback(event => {
3478
+ if (event.pointerId !== activePointerIdRef.current) {
3479
+ return;
3480
+ }
3481
+
3482
+ // The release can go missing entirely (e.g. the browser drops pointer
3483
+ // capture while the scrollbar is hidden mid-drag), leaving the drag
3484
+ // latched so a buttonless hover over the thumb scrolls the viewport.
3485
+ // Treat a move without the primary button held (`buttons` bit 1 unset)
3486
+ // as the missed release.
3487
+ if (event.buttons % 2 === 0) {
3488
+ handlePointerUp(event);
3489
+ return;
3839
3490
  }
3491
+ const viewportEl = viewportRef.current;
3492
+ if (!viewportEl) {
3493
+ return;
3494
+ }
3495
+ const vertical = currentOrientationRef.current === 'vertical';
3496
+ const thumbEl = vertical ? thumbYRef.current : thumbXRef.current;
3497
+ const scrollbarEl = vertical ? scrollbarYRef.current : scrollbarXRef.current;
3498
+ if (!thumbEl || !scrollbarEl) {
3499
+ return;
3500
+ }
3501
+ const axis = vertical ? 'y' : 'x';
3502
+ const scrollbarOffset = getOffset(scrollbarEl, 'padding', axis);
3503
+ const thumbOffset = getOffset(thumbEl, 'margin', axis);
3504
+ const thumbSizePx = vertical ? thumbEl.offsetHeight : thumbEl.offsetWidth;
3505
+ const trackSize = vertical ? scrollbarEl.offsetHeight : scrollbarEl.offsetWidth;
3506
+ const maxThumbOffset = trackSize - thumbSizePx - scrollbarOffset - thumbOffset;
3507
+ // A short or heavily padded track can drive `maxThumbOffset` to zero or
3508
+ // negative once the thumb hits its `MIN_THUMB_SIZE` floor. Dividing by it
3509
+ // would yield a non-finite (`Infinity`/`NaN`) or inverted scroll position.
3510
+ const delta = vertical ? event.clientY - startYRef.current : event.clientX - startXRef.current;
3511
+ const scrollRatio = maxThumbOffset <= 0 ? 0 : delta / maxThumbOffset;
3512
+ const scrollableSize = vertical ? viewportEl.scrollHeight : viewportEl.scrollWidth;
3513
+ const viewportSize = vertical ? viewportEl.clientHeight : viewportEl.clientWidth;
3514
+ const startScroll = vertical ? startScrollTopRef.current : startScrollLeftRef.current;
3515
+ const nextScroll = startScroll + scrollRatio * (scrollableSize - viewportSize);
3516
+ if (vertical) {
3517
+ viewportEl.scrollTop = nextScroll;
3518
+ } else {
3519
+ viewportEl.scrollLeft = nextScroll;
3520
+ }
3521
+ event.preventDefault();
3522
+ startScrolling(vertical);
3840
3523
  });
3841
3524
  function handleTouchModalityChange(event) {
3842
3525
  setTouchModality(event.pointerType === 'touch');
@@ -3868,8 +3551,8 @@ const ScrollAreaRoot = /*#__PURE__*/React__namespace.forwardRef(function ScrollA
3868
3551
  },
3869
3552
  style: {
3870
3553
  position: 'relative',
3871
- [ScrollAreaRootCssVars.scrollAreaCornerHeight]: `${cornerSize.height}px`,
3872
- [ScrollAreaRootCssVars.scrollAreaCornerWidth]: `${cornerSize.width}px`
3554
+ ['--scroll-area-corner-height']: `${cornerSize.height}px`,
3555
+ ['--scroll-area-corner-width']: `${cornerSize.width}px`
3873
3556
  }
3874
3557
  };
3875
3558
  const element = useRenderElement.useRenderElement('div', componentProps, {
@@ -3883,6 +3566,7 @@ const ScrollAreaRoot = /*#__PURE__*/React__namespace.forwardRef(function ScrollA
3883
3566
  handlePointerMove,
3884
3567
  handlePointerUp,
3885
3568
  handleScroll,
3569
+ disableViewportSnap,
3886
3570
  cornerSize,
3887
3571
  setCornerSize,
3888
3572
  thumbSize,
@@ -3892,13 +3576,10 @@ const ScrollAreaRoot = /*#__PURE__*/React__namespace.forwardRef(function ScrollA
3892
3576
  touchModality,
3893
3577
  cornerRef,
3894
3578
  scrollingX,
3895
- setScrollingX,
3896
3579
  scrollingY,
3897
- setScrollingY,
3898
3580
  hovering,
3899
3581
  setHovering,
3900
3582
  viewportRef,
3901
- rootRef,
3902
3583
  scrollbarYRef,
3903
3584
  scrollbarXRef,
3904
3585
  thumbYRef,
@@ -3915,7 +3596,7 @@ const ScrollAreaRoot = /*#__PURE__*/React__namespace.forwardRef(function ScrollA
3915
3596
  yStart,
3916
3597
  yEnd
3917
3598
  }
3918
- }), [handlePointerDown, handlePointerMove, handlePointerUp, handleScroll, cornerSize, thumbSize, hasMeasuredScrollbar, touchModality, scrollingX, setScrollingX, scrollingY, setScrollingY, hovering, setHovering, rootId, hiddenState, overflowEdges, state, xStart, xEnd, yStart, yEnd]);
3599
+ }), [handlePointerDown, handlePointerMove, handlePointerUp, handleScroll, disableViewportSnap, cornerSize, thumbSize, hasMeasuredScrollbar, touchModality, scrollingX, scrollingY, hovering, rootId, hiddenState, overflowEdges, state, xStart, xEnd, yStart, yEnd]);
3919
3600
  return /*#__PURE__*/jsxRuntime.jsxs(ScrollAreaRootContext.Provider, {
3920
3601
  value: contextValue,
3921
3602
  children: [!disableStyleElements && scrollEdges.styleDisableScrollbar.getElement(nonce), element]
@@ -3923,20 +3604,17 @@ const ScrollAreaRoot = /*#__PURE__*/React__namespace.forwardRef(function ScrollA
3923
3604
  });
3924
3605
  if (process.env.NODE_ENV !== "production") ScrollAreaRoot.displayName = "ScrollAreaRoot";
3925
3606
  function normalizeOverflowEdgeThreshold(threshold) {
3926
- if (typeof threshold === 'number') {
3927
- const value = Math.max(0, threshold);
3928
- return {
3929
- xStart: value,
3930
- xEnd: value,
3931
- yStart: value,
3932
- yEnd: value
3933
- };
3934
- }
3607
+ const thresholds = typeof threshold === 'number' ? {
3608
+ xStart: threshold,
3609
+ xEnd: threshold,
3610
+ yStart: threshold,
3611
+ yEnd: threshold
3612
+ } : threshold;
3935
3613
  return {
3936
- xStart: Math.max(0, threshold?.xStart || 0),
3937
- xEnd: Math.max(0, threshold?.xEnd || 0),
3938
- yStart: Math.max(0, threshold?.yStart || 0),
3939
- yEnd: Math.max(0, threshold?.yEnd || 0)
3614
+ xStart: Math.max(0, thresholds?.xStart || 0),
3615
+ xEnd: Math.max(0, thresholds?.xEnd || 0),
3616
+ yStart: Math.max(0, thresholds?.yStart || 0),
3617
+ yEnd: Math.max(0, thresholds?.yEnd || 0)
3940
3618
  };
3941
3619
  }
3942
3620
 
@@ -3950,30 +3628,10 @@ function useScrollAreaViewportContext() {
3950
3628
  return context;
3951
3629
  }
3952
3630
 
3953
- let ScrollAreaViewportCssVars = /*#__PURE__*/function (ScrollAreaViewportCssVars) {
3954
- /**
3955
- * The distance from the horizontal start edge in pixels.
3956
- * @type {number}
3957
- */
3958
- ScrollAreaViewportCssVars["scrollAreaOverflowXStart"] = "--scroll-area-overflow-x-start";
3959
- /**
3960
- * The distance from the horizontal end edge in pixels.
3961
- * @type {number}
3962
- */
3963
- ScrollAreaViewportCssVars["scrollAreaOverflowXEnd"] = "--scroll-area-overflow-x-end";
3964
- /**
3965
- * The distance from the vertical start edge in pixels.
3966
- * @type {number}
3967
- */
3968
- ScrollAreaViewportCssVars["scrollAreaOverflowYStart"] = "--scroll-area-overflow-y-start";
3969
- /**
3970
- * The distance from the vertical end edge in pixels.
3971
- * @type {number}
3972
- */
3973
- ScrollAreaViewportCssVars["scrollAreaOverflowYEnd"] = "--scroll-area-overflow-y-end";
3974
- return ScrollAreaViewportCssVars;
3975
- }({});
3631
+ const OVERFLOW_EDGE_VARS = ['--scroll-area-overflow-x-start', '--scroll-area-overflow-x-end', '--scroll-area-overflow-y-start', '--scroll-area-overflow-y-end'];
3976
3632
 
3633
+ // Module-level flag to ensure we only register the CSS properties once,
3634
+ // regardless of how many Scroll Area components are mounted.
3977
3635
  let scrollAreaOverflowVarsRegistered = false;
3978
3636
 
3979
3637
  /**
@@ -3992,7 +3650,7 @@ function removeCSSVariableInheritance() {
3992
3650
  return;
3993
3651
  }
3994
3652
  if (typeof CSS !== 'undefined' && 'registerProperty' in CSS) {
3995
- [ScrollAreaViewportCssVars.scrollAreaOverflowXStart, ScrollAreaViewportCssVars.scrollAreaOverflowXEnd, ScrollAreaViewportCssVars.scrollAreaOverflowYStart, ScrollAreaViewportCssVars.scrollAreaOverflowYEnd].forEach(name => {
3653
+ OVERFLOW_EDGE_VARS.forEach(name => {
3996
3654
  try {
3997
3655
  CSS.registerProperty({
3998
3656
  name,
@@ -4036,12 +3694,11 @@ const ScrollAreaViewport = /*#__PURE__*/React__namespace.forwardRef(function Scr
4036
3694
  hiddenState,
4037
3695
  setHasMeasuredScrollbar,
4038
3696
  handleScroll,
3697
+ touchModality,
4039
3698
  setHovering,
4040
3699
  setOverflowEdges,
4041
- overflowEdges,
4042
3700
  overflowEdgeThreshold,
4043
- scrollingX,
4044
- scrollingY
3701
+ viewportState
4045
3702
  } = useScrollAreaRootContext();
4046
3703
  const direction = useScrollLock.useDirection();
4047
3704
  const programmaticScrollRef = React__namespace.useRef(true);
@@ -4086,18 +3743,12 @@ const ScrollAreaViewport = /*#__PURE__*/React__namespace.forwardRef(function Scr
4086
3743
  let scrollLeftFromStart = 0;
4087
3744
  let scrollLeftFromEnd = 0;
4088
3745
  if (!scrollbarXHidden) {
4089
- let rawScrollLeftFromStart = 0;
4090
- if (direction === 'rtl') {
4091
- rawScrollLeftFromStart = scrollEdges.clamp(-scrollLeft, 0, maxScrollLeft);
4092
- } else {
4093
- rawScrollLeftFromStart = scrollEdges.clamp(scrollLeft, 0, maxScrollLeft);
4094
- }
4095
- scrollLeftFromStart = scrollEdges.normalizeScrollOffset(rawScrollLeftFromStart, maxScrollLeft);
3746
+ // `normalizeScrollOffset` clamps internally.
3747
+ scrollLeftFromStart = scrollEdges.normalizeScrollOffset(direction === 'rtl' ? -scrollLeft : scrollLeft, maxScrollLeft);
4096
3748
  scrollLeftFromEnd = maxScrollLeft - scrollLeftFromStart;
4097
3749
  }
4098
- const rawScrollTopFromStart = !scrollbarYHidden ? scrollEdges.clamp(scrollTop, 0, maxScrollTop) : 0;
4099
- const scrollTopFromStart = !scrollbarYHidden ? scrollEdges.normalizeScrollOffset(rawScrollTopFromStart, maxScrollTop) : 0;
4100
- const scrollTopFromEnd = !scrollbarYHidden ? maxScrollTop - scrollTopFromStart : 0;
3750
+ const scrollTopFromStart = scrollbarYHidden ? 0 : scrollEdges.normalizeScrollOffset(scrollTop, maxScrollTop);
3751
+ const scrollTopFromEnd = scrollbarYHidden ? 0 : maxScrollTop - scrollTopFromStart;
4101
3752
  const nextWidth = scrollbarXHidden ? 0 : viewportWidth;
4102
3753
  const nextHeight = scrollbarYHidden ? 0 : viewportHeight;
4103
3754
  let nextCornerWidth = 0;
@@ -4122,76 +3773,53 @@ const ScrollAreaViewport = /*#__PURE__*/React__namespace.forwardRef(function Scr
4122
3773
  const maxNextHeight = scrollbarYEl ? Math.min(scrollbarYEl.offsetHeight - cornerHeightOffset, idealNextHeight) : idealNextHeight;
4123
3774
  const clampedNextWidth = Math.max(MIN_THUMB_SIZE, maxNextWidth * ratioX);
4124
3775
  const clampedNextHeight = Math.max(MIN_THUMB_SIZE, maxNextHeight * ratioY);
4125
- setThumbSize(prevSize => {
4126
- if (prevSize.height === clampedNextHeight && prevSize.width === clampedNextWidth) {
4127
- return prevSize;
4128
- }
4129
- return {
4130
- width: clampedNextWidth,
4131
- height: clampedNextHeight
4132
- };
4133
- });
3776
+ setThumbSize(prevSize => pickState(prevSize, {
3777
+ width: clampedNextWidth,
3778
+ height: clampedNextHeight
3779
+ }));
4134
3780
 
4135
3781
  // Handle Y (vertical) scroll
4136
3782
  if (scrollbarYEl && thumbYEl) {
4137
3783
  const maxThumbOffsetY = scrollbarYEl.offsetHeight - clampedNextHeight - scrollbarYOffset - thumbYOffset;
4138
- const scrollRangeY = scrollableContentHeight - viewportHeight;
4139
- const scrollRatioY = scrollRangeY === 0 ? 0 : scrollTop / scrollRangeY;
4140
-
4141
- // In Safari, don't allow it to go negative or too far as `scrollTop` considers the rubber
4142
- // band effect.
4143
- const thumbOffsetY = Math.min(maxThumbOffsetY, Math.max(0, scrollRatioY * maxThumbOffsetY));
3784
+ const thumbOffsetY = applyOverscrollThumb(thumbYEl, '--scroll-area-thumb-height', scrollTop, maxScrollTop, scrollableContentHeight, clampedNextHeight, maxThumbOffsetY);
4144
3785
  thumbYEl.style.transform = `translate3d(0,${thumbOffsetY}px,0)`;
4145
3786
  }
4146
3787
 
4147
3788
  // Handle X (horizontal) scroll
4148
3789
  if (scrollbarXEl && thumbXEl) {
4149
3790
  const maxThumbOffsetX = scrollbarXEl.offsetWidth - clampedNextWidth - scrollbarXOffset - thumbXOffset;
4150
- const scrollRangeX = scrollableContentWidth - viewportWidth;
4151
- const scrollRatioX = scrollRangeX === 0 ? 0 : scrollLeft / scrollRangeX;
4152
-
4153
- // In Safari, don't allow it to go negative or too far as `scrollLeft` considers the rubber
4154
- // band effect.
4155
- const thumbOffsetX = direction === 'rtl' ? scrollEdges.clamp(scrollRatioX * maxThumbOffsetX, -maxThumbOffsetX, 0) : scrollEdges.clamp(scrollRatioX * maxThumbOffsetX, 0, maxThumbOffsetX);
4156
- thumbXEl.style.transform = `translate3d(${thumbOffsetX}px,0,0)`;
4157
- }
4158
- const overflowMetricsPx = [[ScrollAreaViewportCssVars.scrollAreaOverflowXStart, scrollLeftFromStart], [ScrollAreaViewportCssVars.scrollAreaOverflowXEnd, scrollLeftFromEnd], [ScrollAreaViewportCssVars.scrollAreaOverflowYStart, scrollTopFromStart], [ScrollAreaViewportCssVars.scrollAreaOverflowYEnd, scrollTopFromEnd]];
4159
- for (const [cssVar, value] of overflowMetricsPx) {
4160
- viewportEl.style.setProperty(cssVar, `${value}px`);
4161
- }
3791
+ // RTL scrolls from 0 down to `-maxScrollLeft`; measure from the inline start edge so the
3792
+ // overscroll math is direction-agnostic, then flip the resulting offset back below.
3793
+ const scrollFromStart = direction === 'rtl' ? -scrollLeft : scrollLeft;
3794
+ const offsetX = applyOverscrollThumb(thumbXEl, '--scroll-area-thumb-width', scrollFromStart, maxScrollLeft, scrollableContentWidth, clampedNextWidth, maxThumbOffsetX);
3795
+ thumbXEl.style.transform = `translate3d(${direction === 'rtl' ? -offsetX : offsetX}px,0,0)`;
3796
+ }
3797
+ const overflowMetricsPx = [scrollLeftFromStart, scrollLeftFromEnd, scrollTopFromStart, scrollTopFromEnd];
3798
+ OVERFLOW_EDGE_VARS.forEach((cssVar, index) => {
3799
+ viewportEl.style.setProperty(cssVar, `${overflowMetricsPx[index]}px`);
3800
+ });
4162
3801
  if (cornerEl) {
4163
- if (scrollbarXHidden || scrollbarYHidden) {
4164
- setCornerSize({
4165
- width: 0,
4166
- height: 0
4167
- });
4168
- } else if (!scrollbarXHidden && !scrollbarYHidden) {
4169
- setCornerSize({
4170
- width: nextCornerWidth,
4171
- height: nextCornerHeight
4172
- });
4173
- }
4174
- }
4175
- setHiddenState(prevState => mergeHiddenState(prevState, nextHiddenState));
3802
+ // Bail when the size is unchanged (like `setThumbSize` above); otherwise a
3803
+ // fresh object literal on every scroll frame rebuilds the root context and
3804
+ // re-renders every scroll-area part.
3805
+ // `nextCornerWidth`/`nextCornerHeight` stay 0 when either scrollbar is hidden.
3806
+ setCornerSize(prevSize => pickState(prevSize, {
3807
+ width: nextCornerWidth,
3808
+ height: nextCornerHeight
3809
+ }));
3810
+ }
3811
+ setHiddenState(prevState => pickState(prevState, nextHiddenState));
4176
3812
  const nextOverflowEdges = {
4177
3813
  xStart: !scrollbarXHidden && scrollLeftFromStart > overflowEdgeThreshold.xStart,
4178
3814
  xEnd: !scrollbarXHidden && scrollLeftFromEnd > overflowEdgeThreshold.xEnd,
4179
3815
  yStart: !scrollbarYHidden && scrollTopFromStart > overflowEdgeThreshold.yStart,
4180
3816
  yEnd: !scrollbarYHidden && scrollTopFromEnd > overflowEdgeThreshold.yEnd
4181
3817
  };
4182
- setOverflowEdges(prev => {
4183
- if (prev.xStart === nextOverflowEdges.xStart && prev.xEnd === nextOverflowEdges.xEnd && prev.yStart === nextOverflowEdges.yStart && prev.yEnd === nextOverflowEdges.yEnd) {
4184
- return prev;
4185
- }
4186
- return nextOverflowEdges;
4187
- });
3818
+ setOverflowEdges(prev => pickState(prev, nextOverflowEdges));
4188
3819
  });
4189
3820
  useScrollLock.useIsoLayoutEffect(() => {
4190
- if (!viewportRef.current) {
4191
- return;
4192
- }
4193
3821
  removeCSSVariableInheritance();
4194
- }, [viewportRef]);
3822
+ }, []);
4195
3823
  useScrollLock.useIsoLayoutEffect(() => {
4196
3824
  // Wait for scrollbar and thumb refs after hidden-state toggles, refresh math on direction
4197
3825
  // flips, and re-evaluate overflow edges when the threshold changes.
@@ -4234,6 +3862,10 @@ const ScrollAreaViewport = /*#__PURE__*/React__namespace.forwardRef(function Scr
4234
3862
  if (animations.length === 0) {
4235
3863
  return;
4236
3864
  }
3865
+
3866
+ // `allSettled` never rejects, but `computeThumbPosition` can still run against a
3867
+ // torn-down tree once the animations resolve. Swallow instead of leaking an unhandled
3868
+ // rejection; `void` alone would only silence the floating-promise lint.
4237
3869
  Promise.allSettled(animations.map(animation => animation.finished)).then(computeThumbPosition).catch(() => {});
4238
3870
  });
4239
3871
  return () => {
@@ -4261,7 +3893,13 @@ const ScrollAreaViewport = /*#__PURE__*/React__namespace.forwardRef(function Scr
4261
3893
  return;
4262
3894
  }
4263
3895
  computeThumbPosition();
4264
- if (!programmaticScrollRef.current) {
3896
+
3897
+ // WebKit consumes a touch that catches an in-flight momentum scroll or
3898
+ // rubber-band bounce without dispatching any DOM events for the whole
3899
+ // gesture (not even `touchstart`), so scrolls cannot be attributed to
3900
+ // the user through events. Treat every scroll in touch modality as
3901
+ // user-driven instead.
3902
+ if (touchModality || !programmaticScrollRef.current) {
4265
3903
  handleScroll({
4266
3904
  x: viewportRef.current.scrollLeft,
4267
3905
  y: viewportRef.current.scrollTop
@@ -4279,21 +3917,10 @@ const ScrollAreaViewport = /*#__PURE__*/React__namespace.forwardRef(function Scr
4279
3917
  });
4280
3918
  },
4281
3919
  onWheel: handleUserInteraction,
4282
- onTouchMove: handleUserInteraction,
4283
3920
  onPointerMove: handleUserInteraction,
4284
3921
  onPointerEnter: handleUserInteraction,
4285
3922
  onKeyDown: handleUserInteraction
4286
3923
  };
4287
- const viewportState = React__namespace.useMemo(() => ({
4288
- scrolling: scrollingX || scrollingY,
4289
- hasOverflowX: !hiddenState.x,
4290
- hasOverflowY: !hiddenState.y,
4291
- overflowXStart: overflowEdges.xStart,
4292
- overflowXEnd: overflowEdges.xEnd,
4293
- overflowYStart: overflowEdges.yStart,
4294
- overflowYEnd: overflowEdges.yEnd,
4295
- cornerHidden: hiddenState.corner
4296
- }), [scrollingX, scrollingY, hiddenState.x, hiddenState.y, hiddenState.corner, overflowEdges]);
4297
3924
  const element = useRenderElement.useRenderElement('div', componentProps, {
4298
3925
  ref: [forwardedRef, viewportRef],
4299
3926
  state: viewportState,
@@ -4318,16 +3945,40 @@ function getHiddenState(viewport) {
4318
3945
  corner: y || x
4319
3946
  };
4320
3947
  }
4321
- function mergeHiddenState(prevState, nextState) {
4322
- if (prevState.y === nextState.y && prevState.x === nextState.x && prevState.corner === nextState.corner) {
4323
- return prevState;
3948
+
3949
+ /**
3950
+ * Returns `prev` when `next` is shallow-equal to it so setState bails out and
3951
+ * scroll-frame updates don't rebuild the root context.
3952
+ */
3953
+ function pickState(prev, next) {
3954
+ for (const key in next) {
3955
+ if (prev[key] !== next[key]) {
3956
+ return next;
3957
+ }
4324
3958
  }
4325
- return nextState;
3959
+ return prev;
3960
+ }
3961
+
3962
+ /**
3963
+ * Sizes the thumb and returns its axis offset. On overscroll (Safari rubber-band only) it shrinks
3964
+ * against the pinned edge, damped by `content / (content + overscroll)` to match native feedback;
3965
+ * the size flows through the thumb-size variable so the resting `var(...)` still applies.
3966
+ */
3967
+ function applyOverscrollThumb(thumbEl, sizeVar, scrollFromStart, maxScroll, content, size, maxThumbOffset) {
3968
+ const clamped = scrollEdges.clamp(scrollFromStart, 0, maxScroll);
3969
+ const overscroll = scrollFromStart - clamped;
3970
+ const nextSize = Math.max(MIN_THUMB_SIZE, size * content / (content + Math.abs(overscroll)));
3971
+
3972
+ // Passing an empty string removes the override, restoring the resting `var(...)` size.
3973
+ thumbEl.style.setProperty(sizeVar, overscroll ? `${nextSize}px` : '');
3974
+
3975
+ // Slide proportionally; at the end edge push down by the shrink so the thumb stays pinned to
3976
+ // it, while a start overscroll pins to offset 0.
3977
+ const offset = maxScroll ? clamped / maxScroll * maxThumbOffset : 0;
3978
+ return offset + (overscroll > 0 ? size - nextSize : 0);
4326
3979
  }
4327
3980
 
4328
3981
  exports.DialogPortal = DialogPortal;
4329
- exports.DialogRoot = DialogRoot;
4330
- exports.DialogStore = DialogStore;
4331
3982
  exports.DialogTrigger = DialogTrigger;
4332
3983
  exports.DialogViewport = DialogViewport;
4333
3984
  exports.DrawerBackdrop = DrawerBackdrop;
@@ -4342,11 +3993,12 @@ exports.DrawerRoot = DrawerRoot;
4342
3993
  exports.DrawerTrigger = DrawerTrigger;
4343
3994
  exports.DrawerViewport = DrawerViewport;
4344
3995
  exports.DrawerVirtualKeyboardContext = DrawerVirtualKeyboardContext;
3996
+ exports.NullStore = NullStore;
4345
3997
  exports.ScrollAreaRoot = ScrollAreaRoot;
4346
- exports.ScrollAreaRootCssVars = ScrollAreaRootCssVars;
4347
3998
  exports.ScrollAreaViewport = ScrollAreaViewport;
4348
3999
  exports.createInitialPopupStoreState = createInitialPopupStoreState;
4349
- exports.findScrollableTouchTarget = findScrollableTouchTarget;
4000
+ exports.createNullDialogStore = createNullDialogStore;
4001
+ exports.dialogStateAttributesMapping = dialogStateAttributesMapping;
4350
4002
  exports.getDisplacement = getDisplacement;
4351
4003
  exports.getElementAtPoint = getElementAtPoint;
4352
4004
  exports.getElementTransform = getElementTransform;
@@ -4357,6 +4009,8 @@ exports.useDialogPortalContext = useDialogPortalContext;
4357
4009
  exports.useDialogRootContext = useDialogRootContext;
4358
4010
  exports.useDrawerProviderContext = useDrawerProviderContext;
4359
4011
  exports.useDrawerRootContext = useDrawerRootContext;
4012
+ exports.usePopupHandleStore = usePopupHandleStore;
4013
+ exports.useRenderDialogRoot = useRenderDialogRoot;
4360
4014
  exports.useScrollAreaRootContext = useScrollAreaRootContext;
4361
4015
  exports.useScrollAreaViewportContext = useScrollAreaViewportContext;
4362
4016
  exports.useSwipeDismiss = useSwipeDismiss;