@jobber/components 9.6.0 → 9.6.2

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 (187) hide show
  1. package/dist/ActivityIndicator-cjs.js +1 -0
  2. package/dist/ActivityIndicator-es.js +1 -0
  3. package/dist/Autocomplete/index.cjs +2 -0
  4. package/dist/Autocomplete/index.mjs +2 -0
  5. package/dist/Autocomplete-cjs.js +6 -4
  6. package/dist/Autocomplete-es.js +3 -1
  7. package/dist/Banner-cjs.js +1 -0
  8. package/dist/Banner-es.js +1 -0
  9. package/dist/Card/index.cjs +2 -1
  10. package/dist/Card/index.mjs +2 -1
  11. package/dist/Chip/index.cjs +2 -0
  12. package/dist/Chip/index.mjs +2 -0
  13. package/dist/Chip-cjs.js +1 -0
  14. package/dist/Chip-es.js +1 -0
  15. package/dist/Chips/InternalChipDismissible/hooks/index.cjs +2 -0
  16. package/dist/Chips/InternalChipDismissible/hooks/index.mjs +2 -0
  17. package/dist/Chips/InternalChipDismissible/index.cjs +2 -0
  18. package/dist/Chips/InternalChipDismissible/index.mjs +2 -0
  19. package/dist/Chips/index.cjs +2 -0
  20. package/dist/Chips/index.mjs +2 -0
  21. package/dist/Combobox/index.cjs +4 -2
  22. package/dist/Combobox/index.mjs +4 -2
  23. package/dist/{ComboboxSeparator-cjs.js → ComboboxChipRemove-cjs.js} +455 -501
  24. package/dist/{ComboboxSeparator-es.js → ComboboxChipRemove-es.js} +456 -500
  25. package/dist/ComboboxPrimitive-cjs.js +30 -29
  26. package/dist/ComboboxPrimitive-es.js +3 -2
  27. package/dist/ConfirmationModal/index.cjs +2 -0
  28. package/dist/ConfirmationModal/index.mjs +2 -0
  29. package/dist/ConfirmationModal-cjs.js +1 -0
  30. package/dist/ConfirmationModal-es.js +1 -0
  31. package/dist/DataDump/index.cjs +2 -1
  32. package/dist/DataDump/index.mjs +2 -1
  33. package/dist/DataDump-cjs.js +1 -0
  34. package/dist/DataDump-es.js +1 -0
  35. package/dist/DataList/components/DataListActions/index.cjs +2 -0
  36. package/dist/DataList/components/DataListActions/index.mjs +2 -0
  37. package/dist/DataList/components/DataListBulkActions/index.cjs +2 -0
  38. package/dist/DataList/components/DataListBulkActions/index.mjs +2 -0
  39. package/dist/DataList/components/DataListFilters/components/DataListSort/index.cjs +2 -0
  40. package/dist/DataList/components/DataListFilters/components/DataListSort/index.mjs +2 -0
  41. package/dist/DataList/components/DataListFilters/index.cjs +2 -0
  42. package/dist/DataList/components/DataListFilters/index.mjs +2 -0
  43. package/dist/DataList/components/DataListHeader/index.cjs +2 -0
  44. package/dist/DataList/components/DataListHeader/index.mjs +2 -0
  45. package/dist/DataList/components/DataListItem/index.cjs +2 -0
  46. package/dist/DataList/components/DataListItem/index.mjs +2 -0
  47. package/dist/DataList/components/DataListItemActions/index.cjs +2 -0
  48. package/dist/DataList/components/DataListItemActions/index.mjs +2 -0
  49. package/dist/DataList/components/DataListItemActionsOverflow/index.cjs +2 -0
  50. package/dist/DataList/components/DataListItemActionsOverflow/index.mjs +2 -0
  51. package/dist/DataList/components/DataListItems/index.cjs +2 -0
  52. package/dist/DataList/components/DataListItems/index.mjs +2 -0
  53. package/dist/DataList/components/DataListLayout/index.cjs +2 -0
  54. package/dist/DataList/components/DataListLayout/index.mjs +2 -0
  55. package/dist/DataList/components/DataListLayoutActions/index.cjs +2 -0
  56. package/dist/DataList/components/DataListLayoutActions/index.mjs +2 -0
  57. package/dist/DataList/index.cjs +2 -0
  58. package/dist/DataList/index.mjs +2 -0
  59. package/dist/DataTable-cjs.js +1 -0
  60. package/dist/DataTable-es.js +1 -0
  61. package/dist/DatePicker/index.cjs +2 -0
  62. package/dist/DatePicker/index.mjs +2 -0
  63. package/dist/DatePicker-cjs.js +8 -7
  64. package/dist/DatePicker-es.js +6 -5
  65. package/dist/Dialog/index.cjs +2 -1
  66. package/dist/Dialog/index.mjs +2 -1
  67. package/dist/DrawerDescription-cjs.js +99 -268
  68. package/dist/DrawerDescription-es.js +103 -270
  69. package/dist/DrawerGrid-cjs.js +1 -0
  70. package/dist/DrawerGrid-es.js +1 -0
  71. package/dist/DrawerTitle-cjs.js +6 -2
  72. package/dist/DrawerTitle-es.js +7 -3
  73. package/dist/FeatureSwitch-cjs.js +1 -0
  74. package/dist/FeatureSwitch-es.js +1 -0
  75. package/dist/FieldDescription-cjs.js +181 -184
  76. package/dist/FieldDescription-es.js +184 -187
  77. package/dist/FilterPicker/components/FilterPickerActivator/index.cjs +2 -0
  78. package/dist/FilterPicker/components/FilterPickerActivator/index.mjs +2 -0
  79. package/dist/FilterPicker/components/FilterPickerContent/index.cjs +2 -0
  80. package/dist/FilterPicker/components/FilterPickerContent/index.mjs +2 -0
  81. package/dist/FilterPicker/components/FilterPickerTrigger/index.cjs +2 -0
  82. package/dist/FilterPicker/components/FilterPickerTrigger/index.mjs +2 -0
  83. package/dist/FilterPicker/index.cjs +2 -0
  84. package/dist/FilterPicker/index.mjs +2 -0
  85. package/dist/FilterPickerContent-cjs.js +5 -4
  86. package/dist/FilterPickerContent-es.js +2 -1
  87. package/dist/FormFieldWrapper-cjs.js +1 -0
  88. package/dist/FormFieldWrapper-es.js +1 -0
  89. package/dist/FormatFile/index.cjs +2 -0
  90. package/dist/FormatFile/index.mjs +2 -0
  91. package/dist/FormatFile-cjs.js +1 -0
  92. package/dist/FormatFile-es.js +1 -0
  93. package/dist/Gallery/index.cjs +2 -0
  94. package/dist/Gallery/index.mjs +2 -0
  95. package/dist/InputAvatar-cjs.js +1 -0
  96. package/dist/InputAvatar-es.js +1 -0
  97. package/dist/InputDate/index.cjs +2 -0
  98. package/dist/InputDate/index.mjs +2 -0
  99. package/dist/InputDate-cjs.js +1 -0
  100. package/dist/InputDate-es.js +1 -0
  101. package/dist/InputNumber/index.cjs +2 -1
  102. package/dist/InputNumber/index.mjs +2 -1
  103. package/dist/InternalChipDismissible-cjs.js +1 -0
  104. package/dist/InternalChipDismissible-es.js +1 -0
  105. package/dist/LightBox/index.cjs +2 -0
  106. package/dist/LightBox/index.mjs +2 -0
  107. package/dist/LightBox-cjs.js +1 -0
  108. package/dist/LightBox-es.js +1 -0
  109. package/dist/Menu/index.cjs +2 -1
  110. package/dist/Menu/index.mjs +2 -1
  111. package/dist/Menu-cjs.js +6 -4
  112. package/dist/Menu-es.js +3 -1
  113. package/dist/MenuSubmenuTrigger-cjs.js +164 -206
  114. package/dist/MenuSubmenuTrigger-es.js +152 -194
  115. package/dist/Modal/index.cjs +3 -1
  116. package/dist/Modal/index.mjs +4 -2
  117. package/dist/NumberFieldInput-cjs.js +308 -187
  118. package/dist/NumberFieldInput-es.js +308 -187
  119. package/dist/Page/index.cjs +2 -1
  120. package/dist/Page/index.mjs +2 -1
  121. package/dist/Page-cjs.js +1 -0
  122. package/dist/Page-es.js +1 -0
  123. package/dist/Popover/index.cjs +2 -0
  124. package/dist/Popover/index.mjs +2 -0
  125. package/dist/Popover-cjs.js +9 -7
  126. package/dist/Popover-es.js +3 -1
  127. package/dist/ProgressBar-cjs.js +1 -0
  128. package/dist/ProgressBar-es.js +1 -0
  129. package/dist/ScrollAreaViewport-cjs.js +1093 -747
  130. package/dist/ScrollAreaViewport-es.js +1081 -734
  131. package/dist/Select/index.cjs +2 -1
  132. package/dist/Select/index.mjs +2 -1
  133. package/dist/SelectGroupLabel-cjs.js +306 -207
  134. package/dist/SelectGroupLabel-es.js +302 -203
  135. package/dist/SideDrawer-cjs.js +1 -0
  136. package/dist/SideDrawer-es.js +1 -0
  137. package/dist/Spinner-cjs.js +1 -0
  138. package/dist/Spinner-es.js +1 -0
  139. package/dist/Tooltip/index.cjs +2 -0
  140. package/dist/Tooltip/index.mjs +2 -0
  141. package/dist/Tooltip-cjs.js +7 -6
  142. package/dist/Tooltip-es.js +2 -1
  143. package/dist/dialogReturnFocus-cjs.js +1 -1
  144. package/dist/dialogReturnFocus-es.js +2 -2
  145. package/dist/floating-ui.react-cjs.js +192 -2564
  146. package/dist/floating-ui.react-dom-cjs.js +2103 -0
  147. package/dist/floating-ui.react-dom-es.js +2072 -0
  148. package/dist/floating-ui.react-es.js +156 -2519
  149. package/dist/floating-ui.utils-cjs.js +342 -0
  150. package/dist/floating-ui.utils-es.js +300 -0
  151. package/dist/index.cjs +4 -3
  152. package/dist/index.mjs +4 -3
  153. package/dist/primitives/BottomSheet/index.cjs +1 -1
  154. package/dist/primitives/BottomSheet/index.mjs +1 -1
  155. package/dist/primitives/ComboboxPrimitive/index.cjs +4 -2
  156. package/dist/primitives/ComboboxPrimitive/index.mjs +4 -2
  157. package/dist/primitives/SelectPrimitive/index.cjs +2 -0
  158. package/dist/primitives/SelectPrimitive/index.mjs +2 -0
  159. package/dist/primitives/index.cjs +3 -2
  160. package/dist/primitives/index.mjs +3 -2
  161. package/dist/scrollEdges-cjs.js +11 -1
  162. package/dist/scrollEdges-es.js +11 -1
  163. package/dist/sharedHelpers/i18n/index.cjs +1 -0
  164. package/dist/sharedHelpers/i18n/index.d.ts +1 -0
  165. package/dist/sharedHelpers/i18n/index.mjs +1 -1
  166. package/dist/sharedHelpers/i18n/releasedLanguages.d.ts +12 -0
  167. package/dist/sharedHelpers/i18n/useReleasedLocale.d.ts +6 -0
  168. package/dist/showToast-cjs.js +1 -1
  169. package/dist/showToast-es.js +1 -1
  170. package/dist/testUtils/releasedLanguages.d.ts +16 -0
  171. package/dist/unstyledPrimitives/index.cjs +478 -720
  172. package/dist/unstyledPrimitives/index.mjs +421 -663
  173. package/dist/useAtlantisI18n-cjs.js +44 -11
  174. package/dist/useAtlantisI18n-es.js +44 -12
  175. package/dist/useCompositeListItem-cjs.js +361 -2108
  176. package/dist/useCompositeListItem-es.js +331 -2076
  177. package/dist/useLabel-cjs.js +3 -2
  178. package/dist/useLabel-es.js +3 -2
  179. package/dist/useRenderElement-cjs.js +7 -9
  180. package/dist/useRenderElement-es.js +7 -9
  181. package/dist/useScrollLock-cjs.js +344 -819
  182. package/dist/useScrollLock-es.js +296 -732
  183. package/dist/useScrollToActive-cjs.js +5 -4
  184. package/dist/useScrollToActive-es.js +2 -1
  185. package/package.json +3 -3
  186. package/dist/scrollable-cjs.js +0 -58
  187. package/dist/scrollable-es.js +0 -54
@@ -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');
8
9
  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 = state => state.triggerIdProp ?? state.activeTriggerId;
84
- const openSelector = state => state.openProp ?? state.open;
85
- const popupIdSelector = state => {
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 => {
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,81 +97,47 @@ function triggerOwnsOpenPopupOrIsOnlyTrigger(state, triggerId) {
97
97
  }
98
98
  const popupStoreSelectors = {
99
99
  open: openSelector,
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,
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),
106
106
  activeTriggerId: activeTriggerIdSelector,
107
- activeTriggerElement: state => state.mounted ? state.activeTriggerElement : null,
107
+ activeTriggerElement: useScrollLock.createSelector(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: (state, triggerId) => triggerId !== undefined && activeTriggerIdSelector(state) === triggerId,
112
+ isTriggerActive: useScrollLock.createSelector((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: (state, triggerId) => triggerOwnsOpenPopup(state, triggerId),
116
+ isOpenedByTrigger: useScrollLock.createSelector((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: (state, triggerId) => triggerId !== undefined && activeTriggerIdSelector(state) === triggerId && state.mounted,
121
- triggerProps: (state, isActive) => isActive ? state.activeTriggerProps : state.inactiveTriggerProps,
120
+ isMountedByTrigger: useScrollLock.createSelector((state, triggerId) => triggerId !== undefined && activeTriggerIdSelector(state) === triggerId && state.mounted),
121
+ triggerProps: useScrollLock.createSelector((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: (state, triggerId) => triggerOwnsOpenPopupOrIsOnlyTrigger(state, triggerId) ? popupIdSelector(state) : undefined,
126
- popupProps: state => state.popupProps,
127
- popupElement: state => state.popupElement,
128
- positionerElement: state => state.positionerElement
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)
129
129
  };
130
130
 
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
-
131
+ const IsDrawerContext = /*#__PURE__*/React__namespace.createContext(false);
132
+ if (process.env.NODE_ENV !== "production") IsDrawerContext.displayName = "IsDrawerContext";
167
133
  const DialogRootContext = /*#__PURE__*/React__namespace.createContext(undefined);
168
134
  if (process.env.NODE_ENV !== "production") DialogRootContext.displayName = "DialogRootContext";
169
135
  function useDialogRootContext(optional) {
170
- const store = React__namespace.useContext(DialogRootContext);
171
- if (!optional && store === undefined) {
136
+ const dialogRootContext = React__namespace.useContext(DialogRootContext);
137
+ if (optional === false && dialogRootContext === undefined) {
172
138
  throw new Error(process.env.NODE_ENV !== "production" ? 'Base UI: DialogRootContext is missing. Dialog parts must be placed within <Dialog.Root>.' : useRenderElement.formatErrorMessage(27));
173
139
  }
174
- return store;
140
+ return dialogRootContext;
175
141
  }
176
142
 
177
143
  const DialogPortalContext = /*#__PURE__*/React__namespace.createContext(undefined);
@@ -184,26 +150,14 @@ function useDialogPortalContext() {
184
150
  return value;
185
151
  }
186
152
 
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
-
201
153
  const DialogPortal = /*#__PURE__*/React__namespace.forwardRef(function DialogPortal(props, forwardedRef) {
202
154
  const {
203
155
  keepMounted = false,
204
156
  ...portalProps
205
157
  } = props;
206
- const store = useDialogRootContext();
158
+ const {
159
+ store
160
+ } = useDialogRootContext();
207
161
  const mounted = store.useState('mounted');
208
162
  const modal = store.useState('modal');
209
163
  const open = store.useState('open');
@@ -225,6 +179,25 @@ const DialogPortal = /*#__PURE__*/React__namespace.forwardRef(function DialogPor
225
179
  });
226
180
  if (process.env.NODE_ENV !== "production") DialogPortal.displayName = "DialogPortal";
227
181
 
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
+ }
228
201
  function DialogInteractions({
229
202
  store,
230
203
  parentContext,
@@ -260,18 +233,8 @@ function DialogInteractions({
260
233
  if ('button' in event && event.button !== 0) {
261
234
  return false;
262
235
  }
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
- }
236
+ if ('touches' in event && event.touches.length !== 1) {
237
+ return false;
275
238
  }
276
239
  const target = useScrollLock.getTarget(event);
277
240
  if (isTopmost && !disablePointerDismissal) {
@@ -279,9 +242,7 @@ function DialogInteractions({
279
242
  // This supports multiple modal dialogs that aren't nested in the React tree:
280
243
  // https://github.com/mui/base-ui/issues/1320
281
244
  if (modal) {
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;
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;
285
246
  }
286
247
  return true;
287
248
  }
@@ -292,82 +253,73 @@ function DialogInteractions({
292
253
  useScrollLock.useScrollLock(open && modal === true, popupElement);
293
254
 
294
255
  // Listen for nested open/close events on this store to maintain the counts.
295
- // A close notification is an open notification with zeroed counts.
296
256
  store.useContextCallback('onNestedDialogOpen', (dialogCount, drawerCount) => {
297
257
  setOwnNestedOpenDialogs(dialogCount);
298
258
  setOwnNestedOpenDrawers(drawerCount);
299
259
  });
260
+ store.useContextCallback('onNestedDialogClose', () => {
261
+ setOwnNestedOpenDialogs(0);
262
+ setOwnNestedOpenDrawers(0);
263
+ });
300
264
 
301
265
  // Notify parent of our open/close state using parent callbacks, if any
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
- }
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();
309
272
  }
310
273
  return () => {
311
- if (parentContext?.onNestedDialogOpen && open) {
312
- parentContext.onNestedDialogOpen(0, 0);
274
+ if (parentContext?.onNestedDialogClose && open) {
275
+ parentContext.onNestedDialogClose();
313
276
  }
314
277
  };
315
278
  }, [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;
316
284
  useScrollLock.usePopupInteractionProps(store, {
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,
285
+ activeTriggerProps,
286
+ inactiveTriggerProps,
287
+ popupProps,
324
288
  nestedOpenDialogCount: ownNestedOpenDialogs,
325
289
  nestedOpenDrawerCount: ownNestedOpenDrawers
326
290
  });
327
291
  return null;
328
292
  }
329
293
 
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
-
346
294
  const selectors = {
347
295
  ...popupStoreSelectors,
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
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)
358
306
  };
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
-
366
307
  class DialogStore extends useScrollLock.ReactStore {
367
- constructor(initialState, floatingId, nested) {
308
+ constructor(initialState, floatingId, nested = false) {
368
309
  const triggerElements = new useScrollLock.PopupTriggerMap();
369
- const state = createInitialState(initialState, triggerElements, floatingId, nested);
370
- super(state, createInitialContext(triggerElements), selectors);
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);
371
323
  }
372
324
  setOpen = (nextOpen, eventDetails) => {
373
325
  eventDetails.preventUnmountOnClose = () => {
@@ -389,22 +341,20 @@ class DialogStore extends useScrollLock.ReactStore {
389
341
  useScrollLock.setPopupOpenState(updatedState, nextOpen, eventDetails.trigger);
390
342
  this.update(updatedState);
391
343
  };
392
- }
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 */
393
348
 
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);
349
+ return store;
350
+ }
402
351
  }
403
- function createInitialState(initialState, triggerElements, floatingId, nested = false) {
404
- const state = {
352
+ function createInitialState(initialState = {}) {
353
+ return {
405
354
  ...createInitialPopupStoreState(),
406
355
  modal: true,
407
356
  disablePointerDismissal: false,
357
+ popupElement: null,
408
358
  viewportElement: null,
409
359
  descriptionElementId: undefined,
410
360
  titleElementId: undefined,
@@ -415,24 +365,9 @@ function createInitialState(initialState, triggerElements, floatingId, nested =
415
365
  role: 'dialog',
416
366
  ...initialState
417
367
  };
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
- };
433
368
  }
434
369
 
435
- function useRenderDialogRoot(mode, props) {
370
+ function useRenderDialogRoot(props, mode = 'dialog') {
436
371
  const {
437
372
  children,
438
373
  open: openProp,
@@ -451,26 +386,38 @@ function useRenderDialogRoot(mode, props) {
451
386
  const modal = isAlertDialog ? true : modalProp;
452
387
  const disablePointerDismissal = isAlertDialog || disablePointerDismissalProp;
453
388
  const role = isAlertDialog ? 'alertdialog' : 'dialog';
454
- const parentStore = useDialogRootContext(true);
455
- const nested = parentStore != null;
389
+ const parentDialogRootContext = useDialogRootContext(true);
390
+ const nested = Boolean(parentDialogRootContext);
456
391
  const rootState = {
457
392
  modal,
458
393
  disablePointerDismissal,
459
394
  nested,
460
395
  role
461
396
  };
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({
397
+ const store = DialogStore.useStore(handle?.store, {
468
398
  open: defaultOpen,
469
399
  openProp,
470
400
  activeTriggerId: defaultTriggerIdProp,
471
401
  triggerIdProp,
472
402
  ...rootState
473
- }, floatingId, floatingNested), true);
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
+ });
474
421
  store.useControlledProp('openProp', openProp);
475
422
  store.useControlledProp('triggerIdProp', triggerIdProp);
476
423
  store.useSyncedValues(rootState);
@@ -479,31 +426,83 @@ function useRenderDialogRoot(mode, props) {
479
426
  const open = store.useState('open');
480
427
  const mounted = store.useState('mounted');
481
428
  const payload = store.useState('payload');
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]);
429
+ useDialogRoot({
430
+ store,
431
+ actionsRef
432
+ });
491
433
  const shouldRenderInteractions = open || mounted;
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]
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
+ })
504
449
  });
505
450
  }
506
451
 
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
+
507
506
  /**
508
507
  * A positioning container for the dialog popup that can be made scrollable.
509
508
  * Renders a `<div>` element.
@@ -519,7 +518,9 @@ const DialogViewport = /*#__PURE__*/React__namespace.forwardRef(function DialogV
519
518
  ...elementProps
520
519
  } = componentProps;
521
520
  const keepMounted = useDialogPortalContext();
522
- const store = useDialogRootContext();
521
+ const {
522
+ store
523
+ } = useDialogRootContext();
523
524
  const open = store.useState('open');
524
525
  const nested = store.useState('nested');
525
526
  const transitionStatus = store.useState('transitionStatus');
@@ -538,7 +539,7 @@ const DialogViewport = /*#__PURE__*/React__namespace.forwardRef(function DialogV
538
539
  enabled: shouldRender,
539
540
  state,
540
541
  ref: [forwardedRef, setViewportElement],
541
- stateAttributesMapping: dialogStateAttributesMapping,
542
+ stateAttributesMapping: stateAttributesMapping$2,
542
543
  props: [{
543
544
  role: 'presentation',
544
545
  hidden: !mounted,
@@ -569,9 +570,8 @@ const DialogTrigger = /*#__PURE__*/React__namespace.forwardRef(function DialogTr
569
570
  handle,
570
571
  ...elementProps
571
572
  } = componentProps;
572
- const dialogRootStore = useDialogRootContext(true);
573
- const handleStore = usePopupHandleStore(handle);
574
- const store = handleStore ?? dialogRootStore;
573
+ const dialogRootContext = useDialogRootContext(true);
574
+ const store = handle?.store ?? dialogRootContext?.store;
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,7 +593,9 @@ const DialogTrigger = /*#__PURE__*/React__namespace.forwardRef(function DialogTr
593
593
  disabled,
594
594
  native: nativeButton
595
595
  });
596
- const click = useScrollLock.useClick(floatingContext);
596
+ const click = useScrollLock.useClick(floatingContext, {
597
+ enabled: floatingContext != null
598
+ });
597
599
  const interactionTypeProps = useScrollLock.useOpenMethodTriggerProps(() => store.select('open'), interactionType => {
598
600
  store.set('openMethod', interactionType);
599
601
  });
@@ -665,6 +667,11 @@ let DrawerBackdropCssVars = /*#__PURE__*/function (DrawerBackdropCssVars) {
665
667
  return DrawerBackdropCssVars;
666
668
  }({});
667
669
 
670
+ const stateAttributesMapping$1 = {
671
+ ...useScrollLock.popupStateMapping,
672
+ ...useScrollLock.transitionStatusMapping
673
+ };
674
+
668
675
  /**
669
676
  * An overlay displayed beneath the popup.
670
677
  * Renders a `<div>` element.
@@ -679,7 +686,9 @@ const DrawerBackdrop = /*#__PURE__*/React__namespace.forwardRef(function DrawerB
679
686
  forceRender = false,
680
687
  ...elementProps
681
688
  } = componentProps;
682
- const store = useDialogRootContext();
689
+ const {
690
+ store
691
+ } = useDialogRootContext();
683
692
  const open = store.useState('open');
684
693
  const nested = store.useState('nested');
685
694
  const mounted = store.useState('mounted');
@@ -691,7 +700,7 @@ const DrawerBackdrop = /*#__PURE__*/React__namespace.forwardRef(function DrawerB
691
700
  return useRenderElement.useRenderElement('div', componentProps, {
692
701
  state,
693
702
  ref: [store.context.backdropRef, forwardedRef],
694
- stateAttributesMapping: useScrollLock.popupTransitionStateMapping,
703
+ stateAttributesMapping: stateAttributesMapping$1,
695
704
  props: [{
696
705
  role: 'presentation',
697
706
  hidden: !mounted,
@@ -735,8 +744,9 @@ if (process.env.NODE_ENV !== "production") DrawerContent.displayName = "DrawerCo
735
744
 
736
745
  const DrawerProviderContext = /*#__PURE__*/React__namespace.createContext(undefined);
737
746
  if (process.env.NODE_ENV !== "production") DrawerProviderContext.displayName = "DrawerProviderContext";
738
- function useDrawerProviderContext() {
739
- return React__namespace.useContext(DrawerProviderContext);
747
+ function useDrawerProviderContext(optional) {
748
+ const context = React__namespace.useContext(DrawerProviderContext);
749
+ return context;
740
750
  }
741
751
 
742
752
  let DrawerPopupDataAttributes = function (DrawerPopupDataAttributes) {
@@ -749,7 +759,7 @@ let DrawerPopupDataAttributes = function (DrawerPopupDataAttributes) {
749
759
  */
750
760
  DrawerPopupDataAttributes[DrawerPopupDataAttributes["closed"] = useScrollLock.CommonPopupDataAttributes.closed] = "closed";
751
761
  /**
752
- * Present when the drawer begins animating in.
762
+ * Present when the drawer is animating in.
753
763
  */
754
764
  DrawerPopupDataAttributes[DrawerPopupDataAttributes["startingStyle"] = useScrollLock.CommonPopupDataAttributes.startingStyle] = "startingStyle";
755
765
  /**
@@ -788,7 +798,7 @@ const DrawerRootContext = /*#__PURE__*/React__namespace.createContext(undefined)
788
798
  if (process.env.NODE_ENV !== "production") DrawerRootContext.displayName = "DrawerRootContext";
789
799
  function useDrawerRootContext(optional) {
790
800
  const drawerRootContext = React__namespace.useContext(DrawerRootContext);
791
- if (optional !== true && drawerRootContext === undefined) {
801
+ if (optional === false && drawerRootContext === undefined) {
792
802
  throw new Error(process.env.NODE_ENV !== "production" ? 'Base UI: DrawerRootContext is missing. Drawer parts must be placed within <Drawer.Root>.' : useRenderElement.formatErrorMessage(90));
793
803
  }
794
804
  return drawerRootContext;
@@ -829,24 +839,22 @@ function resolveSnapPointValue(snapPoint, viewportHeight, rootFontSize) {
829
839
  }
830
840
  return null;
831
841
  }
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;
842
+ function findClosestSnapPoint(height, points) {
843
+ let closest = null;
838
844
  let closestDistance = Infinity;
839
- for (let index = 0; index < values.length; index += 1) {
840
- const distance = Math.abs(values[index] - target);
845
+ for (const point of points) {
846
+ const distance = Math.abs(point.height - height);
841
847
  if (distance < closestDistance) {
842
848
  closestDistance = distance;
843
- closestIndex = index;
849
+ closest = point;
844
850
  }
845
851
  }
846
- return closestIndex;
852
+ return closest;
847
853
  }
848
854
  function useDrawerSnapPoints() {
849
- const store = useDialogRootContext();
855
+ const {
856
+ store
857
+ } = useDialogRootContext();
850
858
  const {
851
859
  snapPoints,
852
860
  activeSnapPoint,
@@ -859,7 +867,12 @@ function useDrawerSnapPoints() {
859
867
  const measureViewportHeight = useScrollLock.useStableCallback(() => {
860
868
  const doc = useScrollLock.ownerDocument(viewportElement);
861
869
  const html = doc.documentElement;
862
- setViewportHeight(viewportElement ? viewportElement.offsetHeight : html.clientHeight);
870
+ if (viewportElement) {
871
+ setViewportHeight(viewportElement.offsetHeight);
872
+ }
873
+ if (!viewportElement) {
874
+ setViewportHeight(html.clientHeight);
875
+ }
863
876
  const fontSize = parseFloat(getComputedStyle(html).fontSize);
864
877
  if (Number.isFinite(fontSize)) {
865
878
  setRootFontSize(fontSize);
@@ -881,9 +894,12 @@ function useDrawerSnapPoints() {
881
894
  return [];
882
895
  }
883
896
  const maxHeight = Math.min(popupHeight, viewportHeight);
897
+ if (!Number.isFinite(maxHeight) || maxHeight <= 0) {
898
+ return [];
899
+ }
884
900
  const resolved = snapPoints.map(value => {
885
901
  const resolvedHeight = resolveSnapPointValue(value, viewportHeight, rootFontSize);
886
- if (resolvedHeight === null) {
902
+ if (resolvedHeight === null || !Number.isFinite(resolvedHeight)) {
887
903
  return null;
888
904
  }
889
905
  const clampedHeight = scrollEdges.clamp(resolvedHeight, 0, maxHeight);
@@ -911,6 +927,9 @@ function useDrawerSnapPoints() {
911
927
  return deduped;
912
928
  }, [popupHeight, rootFontSize, snapPoints, viewportHeight]);
913
929
  const resolvedActiveSnapPoint = React__namespace.useMemo(() => {
930
+ if (activeSnapPoint === undefined) {
931
+ return resolvedSnapPoints[0];
932
+ }
914
933
  if (activeSnapPoint === null) {
915
934
  return undefined;
916
935
  }
@@ -920,11 +939,11 @@ function useDrawerSnapPoints() {
920
939
  }
921
940
  const maxHeight = Math.min(popupHeight, viewportHeight);
922
941
  const resolvedHeight = resolveSnapPointValue(activeSnapPoint, viewportHeight, rootFontSize);
923
- if (resolvedHeight === null) {
942
+ if (resolvedHeight === null || !Number.isFinite(resolvedHeight)) {
924
943
  return undefined;
925
944
  }
926
945
  const clampedHeight = scrollEdges.clamp(resolvedHeight, 0, maxHeight);
927
- return resolvedSnapPoints[closestSnapPointIndex(resolvedSnapPoints.map(point => point.height), clampedHeight)];
946
+ return findClosestSnapPoint(clampedHeight, resolvedSnapPoints) ?? undefined;
928
947
  }, [activeSnapPoint, popupHeight, resolvedSnapPoints, rootFontSize, viewportHeight]);
929
948
  return {
930
949
  snapPoints,
@@ -939,8 +958,9 @@ function useDrawerSnapPoints() {
939
958
 
940
959
  const DrawerViewportContext = /*#__PURE__*/React__namespace.createContext(null);
941
960
  if (process.env.NODE_ENV !== "production") DrawerViewportContext.displayName = "DrawerViewportContext";
942
- function useDrawerViewportContext() {
943
- return React__namespace.useContext(DrawerViewportContext);
961
+ function useDrawerViewportContext(optional) {
962
+ const context = React__namespace.useContext(DrawerViewportContext);
963
+ return context;
944
964
  }
945
965
 
946
966
  let drawerSwipeVarsRegistered = false;
@@ -998,7 +1018,8 @@ function removeCSSVariableInheritance$1() {
998
1018
  drawerSwipeVarsRegistered = true;
999
1019
  }
1000
1020
  const stateAttributesMapping = {
1001
- ...useScrollLock.popupTransitionStateMapping,
1021
+ ...useScrollLock.popupStateMapping,
1022
+ ...useScrollLock.transitionStatusMapping,
1002
1023
  expanded(value) {
1003
1024
  return value ? {
1004
1025
  [DrawerPopupDataAttributes.expanded]: ''
@@ -1015,9 +1036,9 @@ const stateAttributesMapping = {
1015
1036
  } : null;
1016
1037
  },
1017
1038
  swipeDirection(value) {
1018
- return {
1039
+ return value ? {
1019
1040
  [DrawerPopupDataAttributes.swipeDirection]: value
1020
- };
1041
+ } : null;
1021
1042
  },
1022
1043
  swiping(value) {
1023
1044
  return value ? {
@@ -1041,7 +1062,9 @@ const DrawerPopup = /*#__PURE__*/React__namespace.forwardRef(function DrawerPopu
1041
1062
  initialFocus,
1042
1063
  ...elementProps
1043
1064
  } = componentProps;
1044
- const store = useDialogRootContext();
1065
+ const {
1066
+ store
1067
+ } = useDialogRootContext();
1045
1068
  const popupRef = store.context.popupRef;
1046
1069
  const {
1047
1070
  swipeDirection,
@@ -1080,8 +1103,6 @@ const DrawerPopup = /*#__PURE__*/React__namespace.forwardRef(function DrawerPopu
1080
1103
  const swipeStrength = swipe?.swipeStrength ?? null;
1081
1104
  const [popupHeight, setPopupHeight] = React__namespace.useState(0);
1082
1105
  const popupHeightRef = React__namespace.useRef(0);
1083
-
1084
- /* istanbul ignore else -- process.env.NODE_ENV is a build-time constant. */
1085
1106
  if (process.env.NODE_ENV !== 'production') {
1086
1107
  // eslint-disable-next-line react-hooks/rules-of-hooks
1087
1108
  React__namespace.useEffect(() => {
@@ -1164,7 +1185,7 @@ const DrawerPopup = /*#__PURE__*/React__namespace.forwardRef(function DrawerPopu
1164
1185
  }
1165
1186
  };
1166
1187
  }, [nestedSwipeProgressStore, popupRef]);
1167
- useScrollLock.useIsoLayoutEffect(() => {
1188
+ React__namespace.useEffect(() => {
1168
1189
  if (!open) {
1169
1190
  return undefined;
1170
1191
  }
@@ -1173,7 +1194,7 @@ const DrawerPopup = /*#__PURE__*/React__namespace.forwardRef(function DrawerPopu
1173
1194
  notifyParentFrontmostHeight?.(0);
1174
1195
  };
1175
1196
  }, [frontmostHeight, open, notifyParentFrontmostHeight]);
1176
- useScrollLock.useIsoLayoutEffect(() => {
1197
+ React__namespace.useEffect(() => {
1177
1198
  if (!notifyParentHasNestedDrawer) {
1178
1199
  return undefined;
1179
1200
  }
@@ -1217,7 +1238,7 @@ const DrawerPopup = /*#__PURE__*/React__namespace.forwardRef(function DrawerPopu
1217
1238
  let dragStyles = swipe ? swipe.getDragStyles() : useRenderElement.EMPTY_OBJECT;
1218
1239
  if (shouldApplySnapPoints && swipeDirection === 'down') {
1219
1240
  const baseOffset = activeSnapPointOffset ?? 0;
1220
- const movementValue = Number.parseFloat(String(dragStyles[DrawerPopupCssVars.swipeMovementY]));
1241
+ const movementValue = Number.parseFloat(String(dragStyles[DrawerPopupCssVars.swipeMovementY] ?? 0));
1221
1242
  if (swiping && Number.isFinite(movementValue)) {
1222
1243
  dragStyles = {
1223
1244
  ...dragStyles,
@@ -1327,7 +1348,6 @@ function DrawerRoot(props) {
1327
1348
  state: 'snapPoint'
1328
1349
  });
1329
1350
  const isNestedDrawerOpenRef = React__namespace.useRef(false);
1330
- const swipeAreaActiveRef = React__namespace.useRef(false);
1331
1351
  const setActiveSnapPoint = useScrollLock.useStableCallback((nextSnapPoint, eventDetails) => {
1332
1352
  const resolvedEventDetails = eventDetails ?? useScrollLock.createChangeEventDetails(useScrollLock.none);
1333
1353
  onSnapPointChange?.(nextSnapPoint, resolvedEventDetails);
@@ -1387,7 +1407,6 @@ function DrawerRoot(props) {
1387
1407
  });
1388
1408
  const contextValue = React__namespace.useMemo(() => ({
1389
1409
  swipeDirection,
1390
- swipeAreaActiveRef,
1391
1410
  snapToSequentialPoints,
1392
1411
  snapPoints,
1393
1412
  activeSnapPoint: resolvedActiveSnapPoint,
@@ -1406,28 +1425,30 @@ function DrawerRoot(props) {
1406
1425
  notifyParentSwipingChange,
1407
1426
  notifyParentSwipeProgressChange,
1408
1427
  notifyParentHasNestedDrawer
1409
- }), [resolvedActiveSnapPoint, frontmostHeight, hasNestedDrawer, nestedSwiping, nestedSwipeProgressStore, notifyParentHasNestedDrawer, notifyParentSwipeProgressChange, notifyParentSwipingChange, notifyParentFrontmostHeight, onNestedDrawerPresenceChange, onNestedFrontmostHeightChange, onNestedSwipeProgressChange, onNestedSwipingChange, onPopupHeightChange, popupHeight, setActiveSnapPoint, snapPoints, snapToSequentialPoints, swipeAreaActiveRef, swipeDirection]);
1428
+ }), [resolvedActiveSnapPoint, frontmostHeight, hasNestedDrawer, nestedSwiping, nestedSwipeProgressStore, notifyParentHasNestedDrawer, notifyParentSwipeProgressChange, notifyParentSwipingChange, notifyParentFrontmostHeight, onNestedDrawerPresenceChange, onNestedFrontmostHeightChange, onNestedSwipeProgressChange, onNestedSwipingChange, onPopupHeightChange, popupHeight, setActiveSnapPoint, snapPoints, snapToSequentialPoints, swipeDirection]);
1410
1429
  const resolvedChildren = typeof children === 'function' ? payload => /*#__PURE__*/jsxRuntime.jsxs(React__namespace.Fragment, {
1411
1430
  children: [_DrawerProviderReport || (_DrawerProviderReport = /*#__PURE__*/jsxRuntime.jsx(DrawerProviderReporter, {})), children(payload)]
1412
1431
  }) : /*#__PURE__*/jsxRuntime.jsxs(React__namespace.Fragment, {
1413
1432
  children: [_DrawerProviderReport2 || (_DrawerProviderReport2 = /*#__PURE__*/jsxRuntime.jsx(DrawerProviderReporter, {})), children]
1414
1433
  });
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
- });
1428
1434
  return /*#__PURE__*/jsxRuntime.jsx(DrawerRootContext.Provider, {
1429
1435
  value: contextValue,
1430
- children: dialog
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
+ })
1431
1452
  });
1432
1453
  }
1433
1454
  function createNestedSwipeProgressStore() {
@@ -1454,32 +1475,36 @@ function createNestedSwipeProgressStore() {
1454
1475
  };
1455
1476
  }
1456
1477
  function DrawerProviderReporter() {
1478
+ const drawerId = useScrollLock.useId();
1457
1479
  const providerContext = useDrawerProviderContext();
1458
- const store = useDialogRootContext(false);
1459
- const setDrawerOpen = providerContext?.setDrawerOpen;
1460
- const removeDrawer = providerContext?.removeDrawer;
1480
+ const {
1481
+ store
1482
+ } = useDialogRootContext(false);
1461
1483
  const open = store.useState('open');
1462
1484
  const nestedOpenDialogCount = store.useState('nestedOpenDialogCount');
1463
1485
  const popupElement = store.useState('popupElement');
1464
1486
  const isTopmost = nestedOpenDialogCount === 0;
1465
- useScrollLock.useIsoLayoutEffect(() => {
1466
- if (!removeDrawer) {
1487
+ React__namespace.useEffect(() => {
1488
+ if (!providerContext || drawerId == null) {
1467
1489
  return undefined;
1468
1490
  }
1469
1491
  return () => {
1470
- removeDrawer(store);
1492
+ providerContext.removeDrawer(drawerId);
1471
1493
  };
1472
- }, [removeDrawer, store]);
1473
- useScrollLock.useIsoLayoutEffect(() => {
1474
- setDrawerOpen?.(store, open);
1475
- }, [open, setDrawerOpen, store]);
1494
+ }, [drawerId, providerContext]);
1495
+ React__namespace.useEffect(() => {
1496
+ if (drawerId == null) {
1497
+ return;
1498
+ }
1499
+ providerContext?.setDrawerOpen(drawerId, open);
1500
+ }, [drawerId, open, providerContext]);
1476
1501
  React__namespace.useEffect(() => {
1477
1502
  // CloseWatcher enables the Android back gesture (Chromium-only).
1478
1503
  // Keep this Android-only for now to avoid interfering with Escape/nesting semantics on desktop due to `useDismiss`.
1479
1504
  if (!open || !isTopmost || !useScrollLock.android) {
1480
1505
  return undefined;
1481
1506
  }
1482
- const win = useScrollLock.getWindow(popupElement);
1507
+ const win = floatingUi_utils.getWindow(popupElement);
1483
1508
  const CloseWatcherCtor = win.CloseWatcher;
1484
1509
  if (!CloseWatcherCtor) {
1485
1510
  return undefined;
@@ -1500,11 +1525,53 @@ function DrawerProviderReporter() {
1500
1525
  return null;
1501
1526
  }
1502
1527
 
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;
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;
1508
1575
  }
1509
1576
 
1510
1577
  const DEFAULT_SWIPE_THRESHOLD = 40;
@@ -1529,7 +1596,7 @@ function getDisplacement(direction, deltaX, deltaY) {
1529
1596
  }
1530
1597
  }
1531
1598
  function getElementTransform(element) {
1532
- const computedStyle = useScrollLock.getWindow(element).getComputedStyle(element);
1599
+ const computedStyle = floatingUi_utils.getWindow(element).getComputedStyle(element);
1533
1600
  const transform = computedStyle.transform;
1534
1601
  let translateX = 0;
1535
1602
  let translateY = 0;
@@ -1651,7 +1718,6 @@ function useSwipeDismiss(options) {
1651
1718
  });
1652
1719
  const swipeProgressRef = React__namespace.useRef(0);
1653
1720
  const swipeThresholdRef = React__namespace.useRef(swipeThresholdDefault);
1654
- const swipeThresholdFunctionRef = React__namespace.useRef(null);
1655
1721
  const swipeStartTimeRef = React__namespace.useRef(null);
1656
1722
  const lastDragSampleRef = React__namespace.useRef(null);
1657
1723
  const lastDragVelocityRef = React__namespace.useRef({
@@ -1673,12 +1739,15 @@ function useSwipeDismiss(options) {
1673
1739
  if (!direction) {
1674
1740
  return;
1675
1741
  }
1742
+ if (typeof swipeThresholdProp !== 'function') {
1743
+ swipeThresholdRef.current = swipeThresholdDefault;
1744
+ return;
1745
+ }
1676
1746
  const element = elementRef.current;
1677
- const thresholdFunction = swipeThresholdFunctionRef.current;
1678
- if (!element || !thresholdFunction) {
1747
+ if (!element) {
1679
1748
  return;
1680
1749
  }
1681
- const value = thresholdFunction({
1750
+ const value = swipeThresholdProp({
1682
1751
  element,
1683
1752
  direction
1684
1753
  });
@@ -1756,7 +1825,6 @@ function useSwipeDismiss(options) {
1756
1825
  setDragDismissed(false);
1757
1826
  updateSwipeProgress(0);
1758
1827
  swipeThresholdRef.current = swipeThresholdDefault;
1759
- swipeThresholdFunctionRef.current = null;
1760
1828
  dragStartPosRef.current = {
1761
1829
  x: 0,
1762
1830
  y: 0
@@ -1797,6 +1865,11 @@ function useSwipeDismiss(options) {
1797
1865
  lastProgressDetailsRef.current = null;
1798
1866
  syncDragStyles(false);
1799
1867
  }, [setSwiping, swipeThresholdDefault, syncDragStyles, updateSwipeProgress]);
1868
+ React__namespace.useEffect(() => {
1869
+ if (typeof swipeThresholdProp !== 'function') {
1870
+ swipeThresholdRef.current = swipeThresholdDefault;
1871
+ }
1872
+ }, [swipeThresholdDefault, swipeThresholdProp]);
1800
1873
  function getPrimaryPointerPosition(event) {
1801
1874
  if ('touches' in event) {
1802
1875
  const touch = event.touches[0];
@@ -1817,19 +1890,19 @@ function useSwipeDismiss(options) {
1817
1890
  return event.pointerType === 'touch';
1818
1891
  }
1819
1892
  function getTargetAtPoint(position, nativeEvent) {
1820
- const root = elementRef.current?.getRootNode();
1821
- const elementAtPoint = getElementAtPoint(root, position.x, position.y);
1893
+ const doc = useScrollLock.ownerDocument(elementRef.current);
1894
+ const elementAtPoint = getElementAtPoint(doc, position.x, position.y);
1822
1895
  const target = elementAtPoint ?? useScrollLock.getTarget(nativeEvent);
1823
1896
  return target;
1824
1897
  }
1825
1898
  function findGestureScrollableTouchTarget(target, root) {
1826
1899
  if (hasHorizontal && !hasVertical) {
1827
- return scrollable.findScrollableTouchTarget(target, root, 'horizontal');
1900
+ return findScrollableTouchTarget(target, root, 'horizontal');
1828
1901
  }
1829
1902
  if (hasVertical && !hasHorizontal) {
1830
- return scrollable.findScrollableTouchTarget(target, root, 'vertical');
1903
+ return findScrollableTouchTarget(target, root, 'vertical');
1831
1904
  }
1832
- return scrollable.findScrollableTouchTarget(target, root, 'vertical') ?? scrollable.findScrollableTouchTarget(target, root, 'horizontal');
1905
+ return findScrollableTouchTarget(target, root, 'vertical') ?? findScrollableTouchTarget(target, root, 'horizontal');
1833
1906
  }
1834
1907
  function startSwipeAtPosition(event, position, startOptions) {
1835
1908
  swipeFromScrollableRef.current = false;
@@ -1850,7 +1923,7 @@ function useSwipeDismiss(options) {
1850
1923
  const element = elementRef.current;
1851
1924
  if (ignoreScrollableAncestors && element && target && scrollAxes.length > 0) {
1852
1925
  const ignoreAncestors = startOptions?.ignoreScrollableAncestors ?? false;
1853
- if (!ignoreAncestors && scrollable.hasScrollableAncestor(target, element, scrollAxes)) {
1926
+ if (!ignoreAncestors && hasScrollableAncestor(target, element, scrollAxes)) {
1854
1927
  return false;
1855
1928
  }
1856
1929
  }
@@ -1861,8 +1934,6 @@ function useSwipeDismiss(options) {
1861
1934
  swipeStartTimeRef.current = getValidTimeStamp(event.timeStamp);
1862
1935
  swipeCancelBaselineRef.current = position;
1863
1936
  lastMovePosRef.current = position;
1864
- swipeThresholdRef.current = swipeThresholdDefault;
1865
- swipeThresholdFunctionRef.current = typeof swipeThresholdProp === 'function' ? swipeThresholdProp : null;
1866
1937
  if (element) {
1867
1938
  elementSizeRef.current = {
1868
1939
  width: element.offsetWidth,
@@ -1927,9 +1998,12 @@ function useSwipeDismiss(options) {
1927
1998
  onCancel?.(event.nativeEvent);
1928
1999
  }
1929
2000
  function applyDirectionalDamping(deltaX, deltaY) {
1930
- const exponent = value => Math.sign(value) * Math.abs(value) ** 0.5;
2001
+ const exponent = value => value >= 0 ? value ** 0.5 : -(Math.abs(value) ** 0.5);
1931
2002
  const dampAxis = (delta, allowNegative, allowPositive) => {
1932
- if (!allowNegative && delta < 0 || !allowPositive && delta > 0) {
2003
+ if (!allowNegative && delta < 0) {
2004
+ return exponent(delta);
2005
+ }
2006
+ if (!allowPositive && delta > 0) {
1933
2007
  return exponent(delta);
1934
2008
  }
1935
2009
  return delta;
@@ -1942,16 +2016,29 @@ function useSwipeDismiss(options) {
1942
2016
  };
1943
2017
  }
1944
2018
  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;
1948
2019
  const absDeltaX = Math.abs(deltaX);
1949
2020
  const absDeltaY = Math.abs(deltaY);
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);
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;
1955
2042
  }
1956
2043
  return null;
1957
2044
  }
@@ -1972,7 +2059,7 @@ function useSwipeDismiss(options) {
1972
2059
  pendingSwipeRef.current = true;
1973
2060
  pendingSwipeStartPosRef.current = startPos;
1974
2061
  swipeFromScrollableRef.current = false;
1975
- sawPrimaryButtonsOnMoveRef.current = !('touches' in event);
2062
+ sawPrimaryButtonsOnMoveRef.current = false;
1976
2063
  const allowedToStart = canStart ? canStart(startPos, {
1977
2064
  nativeEvent: event.nativeEvent,
1978
2065
  direction: primaryDirection
@@ -2000,21 +2087,14 @@ function useSwipeDismiss(options) {
2000
2087
  event.preventDefault();
2001
2088
  }
2002
2089
  if (isFirstPointerMoveRef.current) {
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
- }
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;
2017
2096
  }
2097
+ isFirstPointerMoveRef.current = false;
2018
2098
  }
2019
2099
  const clientX = position.x;
2020
2100
  const clientY = position.y;
@@ -2102,45 +2182,51 @@ function useSwipeDismiss(options) {
2102
2182
  newOffsetY += dampedDelta.y;
2103
2183
  }
2104
2184
  }
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;
2115
2185
  dragOffsetRef.current = {
2116
2186
  x: newOffsetX,
2117
2187
  y: newOffsetY
2118
2188
  };
2119
- if (offsetChanged) {
2120
- syncDragStyles(true);
2121
- }
2189
+ syncDragStyles(true);
2122
2190
  recordDragSample({
2123
2191
  x: newOffsetX,
2124
2192
  y: newOffsetY
2125
2193
  }, getValidTimeStamp(event.timeStamp));
2126
2194
  const dragDeltaX = newOffsetX - initialTransformRef.current.x;
2127
2195
  const dragDeltaY = newOffsetY - initialTransformRef.current.y;
2128
- const progressDetails = {
2129
- deltaX: dragDeltaX,
2130
- deltaY: dragDeltaY,
2131
- direction: intendedSwipeDirectionRef.current
2132
- };
2133
- let progress = 0;
2196
+ const swipeDirectionDetails = intendedSwipeDirectionRef.current;
2134
2197
  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
- }
2198
+ if (!progressDirection) {
2199
+ updateSwipeProgress(0, {
2200
+ deltaX: dragDeltaX,
2201
+ deltaY: dragDeltaY,
2202
+ direction: swipeDirectionDetails
2203
+ });
2204
+ return;
2142
2205
  }
2143
- updateSwipeProgress(progress, progressDetails);
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), {
2226
+ deltaX: dragDeltaX,
2227
+ deltaY: dragDeltaY,
2228
+ direction: swipeDirectionDetails
2229
+ });
2144
2230
  }
2145
2231
  const handleEnd = useScrollLock.useStableCallback(event => {
2146
2232
  if (!enabled) {
@@ -2228,9 +2314,33 @@ function useSwipeDismiss(options) {
2228
2314
  dismissDirection = intendedSwipeDirectionRef.current ?? primaryDirection;
2229
2315
  } else {
2230
2316
  for (const direction of directions) {
2231
- if (getDisplacement(direction, deltaX, deltaY) > swipeThresholdRef.current) {
2232
- shouldClose = true;
2233
- dismissDirection = direction;
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) {
2234
2344
  break;
2235
2345
  }
2236
2346
  }
@@ -2372,29 +2482,25 @@ function useSwipeDismiss(options) {
2372
2482
  });
2373
2483
  });
2374
2484
  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;
2379
2485
  const dragOffset = dragOffsetRef.current;
2380
2486
  const initialTransform = initialTransformRef.current;
2381
2487
  const deltaX = dragOffset.x - initialTransform.x;
2382
2488
  const deltaY = dragOffset.y - initialTransform.y;
2383
- if (!swiping && deltaX === 0 && deltaY === 0 && !dragDismissed) {
2489
+ if (!isSwiping && deltaX === 0 && deltaY === 0 && !dragDismissed) {
2384
2490
  return {
2385
2491
  [movementCssVars.x]: '0px',
2386
2492
  [movementCssVars.y]: '0px'
2387
2493
  };
2388
2494
  }
2389
2495
  return {
2390
- transition: swiping ? 'none' : undefined,
2496
+ transition: isSwiping ? 'none' : undefined,
2391
2497
  // While swiping, freeze the element at its current visual transform so it doesn't snap to the
2392
2498
  // end position.
2393
- transform: swiping ? getDragTransform(dragOffset, initialTransform.scale) : undefined,
2499
+ transform: isSwiping ? getDragTransform(dragOffset, initialTransform.scale) : undefined,
2394
2500
  [movementCssVars.x]: `${deltaX}px`,
2395
2501
  [movementCssVars.y]: `${deltaY}px`
2396
2502
  };
2397
- }, [dragDismissed, movementCssVars]);
2503
+ }, [dragDismissed, isSwiping, movementCssVars]);
2398
2504
  const getPointerProps = React__namespace.useCallback(() => {
2399
2505
  if (!enabled) {
2400
2506
  return {};
@@ -2454,8 +2560,6 @@ const MIN_SWIPE_RELEASE_DURATION_MS = 80;
2454
2560
  const MAX_SWIPE_RELEASE_DURATION_MS = 360;
2455
2561
  const MIN_SWIPE_RELEASE_SCALAR = 0.1;
2456
2562
  const MAX_SWIPE_RELEASE_SCALAR = 1;
2457
- const AXIS_LOCK_SLOP = 6;
2458
- const AXIS_LOCK_BIAS = 2;
2459
2563
  const DRAWER_CONTENT_SELECTOR = `[${DRAWER_CONTENT_ATTRIBUTE}]`;
2460
2564
  /**
2461
2565
  * A positioning container for the drawer popup that can be made scrollable.
@@ -2471,7 +2575,9 @@ const DrawerViewport = /*#__PURE__*/React__namespace.forwardRef(function DrawerV
2471
2575
  children,
2472
2576
  ...elementProps
2473
2577
  } = props;
2474
- const store = useDialogRootContext();
2578
+ const {
2579
+ store
2580
+ } = useDialogRootContext();
2475
2581
  const popupRef = store.context.popupRef;
2476
2582
  const backdropRef = store.context.backdropRef;
2477
2583
  const {
@@ -2479,8 +2585,7 @@ const DrawerViewport = /*#__PURE__*/React__namespace.forwardRef(function DrawerV
2479
2585
  notifyParentSwipingChange,
2480
2586
  notifyParentSwipeProgressChange,
2481
2587
  frontmostHeight,
2482
- snapToSequentialPoints,
2483
- swipeAreaActiveRef
2588
+ snapToSequentialPoints
2484
2589
  } = useDrawerRootContext();
2485
2590
  const providerContext = useDrawerProviderContext();
2486
2591
  const {
@@ -2514,13 +2619,30 @@ const DrawerViewport = /*#__PURE__*/React__namespace.forwardRef(function DrawerV
2514
2619
  const touchScrollStateRef = React__namespace.useRef(null);
2515
2620
  const virtualKeyboard = useDrawerVirtualKeyboardContext();
2516
2621
  const snapPointRange = React__namespace.useMemo(() => {
2517
- if (!snapPoints || snapPoints.length < 2 || resolvedSnapPoints.length < 2 || swipeDirection !== 'down' && swipeDirection !== 'up') {
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) {
2518
2633
  return null;
2519
2634
  }
2520
- const offsets = resolvedSnapPoints.map(point => point.offset).sort((a, b) => a - b);
2521
2635
  const minOffset = offsets[0];
2522
2636
  const nextOffset = offsets[1];
2523
- const range = nextOffset - minOffset;
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
+ }
2524
2646
  return {
2525
2647
  minOffset,
2526
2648
  range
@@ -2539,8 +2661,7 @@ const DrawerViewport = /*#__PURE__*/React__namespace.forwardRef(function DrawerV
2539
2661
  return [swipeDirection];
2540
2662
  }, [snapPoints, swipeDirection]);
2541
2663
  const setSwipeDismissed = useScrollLock.useStableCallback(dismissed => {
2542
- popupRef.current?.toggleAttribute(DrawerPopupDataAttributes.swipeDismiss, dismissed);
2543
- backdropRef.current?.toggleAttribute(DrawerPopupDataAttributes.swipeDismiss, dismissed);
2664
+ setSwipeDismissedElements(popupRef.current, backdropRef.current, dismissed);
2544
2665
  });
2545
2666
  const clearSwipeRelease = useScrollLock.useStableCallback(() => {
2546
2667
  setSwipeDismissed(false);
@@ -2554,7 +2675,11 @@ const DrawerViewport = /*#__PURE__*/React__namespace.forwardRef(function DrawerV
2554
2675
  nestedSwipeActiveRef.current = false;
2555
2676
  notifyParentSwipingChange?.(false);
2556
2677
  });
2557
- const applySwipeProgress = useScrollLock.useStableCallback((resolvedProgress, shouldTrackProgress, notifyParent) => {
2678
+ const applySwipeProgress = useScrollLock.useStableCallback(({
2679
+ resolvedProgress,
2680
+ shouldTrackProgress,
2681
+ notifyParent
2682
+ }) => {
2558
2683
  const isActive = open && !nested && shouldTrackProgress;
2559
2684
  const swipeProgress = isActive ? resolvedProgress : 0;
2560
2685
  const nestedSwipeProgress = open && shouldTrackProgress ? resolvedProgress : 0;
@@ -2572,48 +2697,78 @@ const DrawerViewport = /*#__PURE__*/React__namespace.forwardRef(function DrawerV
2572
2697
  if (!backdropElement) {
2573
2698
  return;
2574
2699
  }
2575
- const showProgress = isActive && swipeProgress > 0;
2576
- backdropElement.style.setProperty(DrawerBackdropCssVars.swipeProgress, showProgress ? `${swipeProgress}` : '0');
2577
- if (showProgress && frontmostHeight > 0) {
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) {
2578
2707
  backdropElement.style.setProperty(DrawerPopupCssVars.height, `${frontmostHeight}px`);
2579
2708
  } else {
2580
2709
  backdropElement.style.removeProperty(DrawerPopupCssVars.height);
2581
2710
  }
2582
2711
  });
2583
- function resolveSwipeRelease(popupElement, direction, deltaX, deltaY, velocityX, velocityY, releaseVelocityX, releaseVelocityY) {
2584
- const size = getBaseSwipeSize(popupElement, direction);
2585
- if (size <= 0) {
2712
+ function resolveSwipeRelease({
2713
+ direction,
2714
+ deltaX,
2715
+ deltaY,
2716
+ velocityX,
2717
+ velocityY,
2718
+ releaseVelocityX,
2719
+ releaseVelocityY
2720
+ }) {
2721
+ if (!direction) {
2586
2722
  return null;
2587
2723
  }
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);
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) {
2730
+ return null;
2731
+ }
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;
2593
2742
  const remainingDistance = Math.max(0, size - translationAlongDirection);
2594
- if (remainingDistance <= 0) {
2743
+ if (!Number.isFinite(remainingDistance) || remainingDistance <= 0) {
2595
2744
  return null;
2596
2745
  }
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) {
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) {
2600
2751
  return null;
2601
2752
  }
2602
2753
  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].
2606
2754
  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
+ }
2607
2758
  const normalizedDuration = (durationMs - MIN_SWIPE_RELEASE_DURATION_MS) / (MAX_SWIPE_RELEASE_DURATION_MS - MIN_SWIPE_RELEASE_DURATION_MS);
2608
- return MIN_SWIPE_RELEASE_SCALAR + normalizedDuration * (MAX_SWIPE_RELEASE_SCALAR - MIN_SWIPE_RELEASE_SCALAR);
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;
2609
2764
  }
2610
2765
  function updateNestedSwipeActive(details) {
2611
2766
  if (nestedSwipeActiveRef.current || !details) {
2612
2767
  return;
2613
2768
  }
2614
2769
  const direction = details.direction ?? swipeDirection;
2615
- const delta = getDisplacement(direction, details.deltaX, details.deltaY);
2616
- if (Math.abs(delta) < MIN_SWIPE_THRESHOLD) {
2770
+ const delta = direction === 'left' || direction === 'right' ? details.deltaX : details.deltaY;
2771
+ if (!Number.isFinite(delta) || Math.abs(delta) < MIN_SWIPE_THRESHOLD) {
2617
2772
  return;
2618
2773
  }
2619
2774
  nestedSwipeActiveRef.current = true;
@@ -2630,17 +2785,20 @@ const DrawerViewport = /*#__PURE__*/React__namespace.forwardRef(function DrawerV
2630
2785
  y: DrawerPopupCssVars.swipeMovementY
2631
2786
  },
2632
2787
  onSwipeStart(event) {
2633
- if ('touches' in event || event.pointerType === 'touch') {
2788
+ if ('touches' in event || 'pointerType' in event && event.pointerType === 'touch') {
2634
2789
  return;
2635
2790
  }
2636
2791
  const popupElement = popupRef.current;
2792
+ if (!popupElement) {
2793
+ return;
2794
+ }
2637
2795
  const doc = useScrollLock.ownerDocument(popupElement);
2638
2796
  const selection = doc.getSelection?.();
2639
2797
  if (!selection || selection.isCollapsed) {
2640
2798
  return;
2641
2799
  }
2642
- const anchorElement = useScrollLock.isElement(selection.anchorNode) ? selection.anchorNode : selection.anchorNode?.parentElement;
2643
- const focusElement = useScrollLock.isElement(selection.focusNode) ? selection.focusNode : selection.focusNode?.parentElement;
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;
2644
2802
  if (!useScrollLock.contains(popupElement, anchorElement) && !useScrollLock.contains(popupElement, focusElement)) {
2645
2803
  return;
2646
2804
  }
@@ -2665,38 +2823,54 @@ const DrawerViewport = /*#__PURE__*/React__namespace.forwardRef(function DrawerV
2665
2823
  return false;
2666
2824
  }
2667
2825
  const doc = popupElement.ownerDocument;
2668
- const elementAtPoint = getElementAtPoint(popupElement.getRootNode(), position.x, position.y);
2826
+ const elementAtPoint = getElementAtPoint(doc, position.x, position.y);
2669
2827
  if (!elementAtPoint || !useScrollLock.contains(popupElement, elementAtPoint)) {
2670
2828
  return false;
2671
2829
  }
2672
2830
  const nativeEvent = details.nativeEvent;
2673
- const touchLike = 'touches' in nativeEvent || nativeEvent.pointerType === 'touch';
2831
+ const touchLike = 'touches' in nativeEvent || 'pointerType' in nativeEvent && nativeEvent.pointerType === 'touch';
2674
2832
  if (touchLike && shouldIgnoreSwipeForTextSelection(doc, popupElement)) {
2675
2833
  return false;
2676
2834
  }
2835
+ if (nativeEvent.type === 'touchstart' && isSwipeIgnoredTarget(elementAtPoint)) {
2836
+ return false;
2837
+ }
2677
2838
  return true;
2678
2839
  },
2679
2840
  onProgress(progress, details) {
2680
2841
  updateNestedSwipeActive(details);
2681
2842
  const hasSnapPoints = Boolean(snapPoints && snapPoints.length > 0);
2682
- if (swipingRef.current && swipeDirection === 'down' && hasSnapPoints && details) {
2843
+ if (swipingRef.current && swipeDirection === 'down' && hasSnapPoints && details && Number.isFinite(details.deltaY)) {
2683
2844
  const popupElement = store.context.popupRef.current;
2684
2845
  if (popupElement) {
2685
2846
  popupElement.style.removeProperty('transform');
2686
2847
  popupElement.style.setProperty(DrawerPopupCssVars.swipeMovementY, `${getSnapPointSwipeMovement(activeSnapPointOffset ?? 0, details.deltaY)}px`);
2687
2848
  }
2688
2849
  }
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;
2689
2854
  let resolvedProgress = progress;
2690
2855
  if (snapPointRange && popupHeight > 0) {
2691
- const baseOffset = activeSnapPointOffset ?? snapPointRange.minOffset;
2692
- const offsetToProgress = nextOffset => scrollEdges.clamp((nextOffset - snapPointRange.minOffset) / snapPointRange.range, 0, 1);
2693
2856
  if (details && Number.isFinite(details.deltaY)) {
2694
- resolvedProgress = offsetToProgress(scrollEdges.clamp(baseOffset + details.deltaY, 0, popupHeight));
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);
2695
2860
  } else if (snapPointProgress !== null) {
2696
2861
  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);
2697
2867
  }
2698
2868
  }
2699
- applySwipeProgress(resolvedProgress, true, true);
2869
+ applySwipeProgress({
2870
+ resolvedProgress,
2871
+ shouldTrackProgress,
2872
+ notifyParent: true
2873
+ });
2700
2874
  },
2701
2875
  onRelease({
2702
2876
  event,
@@ -2708,21 +2882,30 @@ const DrawerViewport = /*#__PURE__*/React__namespace.forwardRef(function DrawerV
2708
2882
  releaseVelocityX,
2709
2883
  releaseVelocityY
2710
2884
  }) {
2711
- const popupElement = store.context.popupRef.current;
2712
- if (!popupElement) {
2713
- clearSwipeRelease();
2714
- return undefined;
2715
- }
2716
- const releasePopupElement = popupElement;
2885
+ const swipeReleasePayload = {
2886
+ deltaX,
2887
+ deltaY,
2888
+ velocityX,
2889
+ velocityY,
2890
+ releaseVelocityX,
2891
+ releaseVelocityY
2892
+ };
2717
2893
  function startSwipeRelease(resolvedDirection) {
2718
2894
  // Start ending transition styles earlier and synchronously to prevent a period where
2719
2895
  // 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
+ }
2720
2900
  finishNestedSwipe();
2721
2901
  setSwipeDismissed(true);
2722
- releasePopupElement.style.removeProperty('transition');
2723
- releasePopupElement.setAttribute(useScrollLock.TransitionStatusDataAttributes.endingStyle, '');
2902
+ popupElement.style.removeProperty('transition');
2903
+ popupElement.setAttribute(useScrollLock.TransitionStatusDataAttributes.endingStyle, '');
2724
2904
  ReactDOM__namespace.flushSync(() => {
2725
- setSwipeRelease(resolveSwipeRelease(releasePopupElement, resolvedDirection, deltaX, deltaY, velocityX, velocityY, releaseVelocityX, releaseVelocityY));
2905
+ setSwipeRelease(resolveSwipeRelease({
2906
+ direction: resolvedDirection,
2907
+ ...swipeReleasePayload
2908
+ }));
2726
2909
  });
2727
2910
  }
2728
2911
  if (!snapPoints || snapPoints.length === 0) {
@@ -2730,16 +2913,29 @@ const DrawerViewport = /*#__PURE__*/React__namespace.forwardRef(function DrawerV
2730
2913
  clearSwipeRelease();
2731
2914
  return undefined;
2732
2915
  }
2733
- const directionalDelta = getDisplacement(direction, deltaX, deltaY);
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;
2734
2928
  if (directionalDelta <= 0) {
2735
2929
  clearSwipeRelease();
2736
2930
  return false;
2737
2931
  }
2738
- if (getDisplacement(direction, velocityX, velocityY) >= FAST_SWIPE_VELOCITY) {
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) {
2739
2935
  startSwipeRelease(direction);
2740
2936
  return true;
2741
2937
  }
2742
- const shouldClose = directionalDelta > getBaseSwipeThreshold(releasePopupElement, direction);
2938
+ const shouldClose = directionalDelta > baseThreshold;
2743
2939
  if (shouldClose) {
2744
2940
  startSwipeRelease(direction);
2745
2941
  } else {
@@ -2751,20 +2947,20 @@ const DrawerViewport = /*#__PURE__*/React__namespace.forwardRef(function DrawerV
2751
2947
  clearSwipeRelease();
2752
2948
  return undefined;
2753
2949
  }
2754
- if (!popupHeight) {
2950
+ if (!popupHeight || resolvedSnapPoints.length === 0) {
2755
2951
  clearSwipeRelease();
2756
- return false;
2952
+ return undefined;
2757
2953
  }
2758
- if (resolvedSnapPoints.length === 0) {
2954
+ const dragDelta = swipeDirection === 'down' ? deltaY : -deltaY;
2955
+ if (!Number.isFinite(dragDelta)) {
2759
2956
  clearSwipeRelease();
2760
2957
  return undefined;
2761
2958
  }
2762
- const dragDelta = swipeDirection === 'down' ? deltaY : -deltaY;
2763
2959
  const dragDirection = Math.sign(dragDelta);
2764
2960
  const releaseDirectionalVelocity = swipeDirection === 'down' ? releaseVelocityY : -releaseVelocityY;
2765
2961
  const fallbackDirectionalVelocity = swipeDirection === 'down' ? velocityY : -velocityY;
2766
- let resolvedDirectionalVelocity = releaseDirectionalVelocity;
2767
- if (dragDirection !== 0 && Math.abs(dragDelta) >= MIN_SWIPE_THRESHOLD) {
2962
+ let resolvedDirectionalVelocity = Number.isFinite(releaseDirectionalVelocity) ? releaseDirectionalVelocity : fallbackDirectionalVelocity;
2963
+ if (dragDirection !== 0 && Math.abs(dragDelta) >= MIN_SWIPE_THRESHOLD && Number.isFinite(resolvedDirectionalVelocity)) {
2768
2964
  const velocityDirection = Math.sign(resolvedDirectionalVelocity);
2769
2965
  if (velocityDirection !== 0 && velocityDirection !== dragDirection) {
2770
2966
  // Ignore touch reversals that would otherwise flip the snap decision.
@@ -2773,20 +2969,39 @@ const DrawerViewport = /*#__PURE__*/React__namespace.forwardRef(function DrawerV
2773
2969
  }
2774
2970
  const currentOffset = activeSnapPointOffset ?? 0;
2775
2971
  const dragTargetOffset = scrollEdges.clamp(currentOffset + dragDelta, 0, popupHeight);
2776
- const velocityOffset = Math.abs(resolvedDirectionalVelocity) >= SNAP_VELOCITY_THRESHOLD ? scrollEdges.clamp(resolvedDirectionalVelocity, -MAX_SNAP_VELOCITY, MAX_SNAP_VELOCITY) * SNAP_VELOCITY_MULTIPLIER : 0;
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;
2777
2973
  const targetOffset = snapToSequentialPoints ? dragTargetOffset : scrollEdges.clamp(dragTargetOffset + velocityOffset, 0, popupHeight);
2778
2974
  const snapPointEventDetails = useScrollLock.createChangeEventDetails(useScrollLock.swipe, event);
2779
2975
  const closeFromSnapPoints = () => {
2780
2976
  pendingSwipeCloseSnapPointRef.current = activeSnapPoint;
2781
- setActiveSnapPoint(null, snapPointEventDetails);
2977
+ setActiveSnapPoint?.(null, snapPointEventDetails);
2782
2978
  startSwipeRelease(swipeDirection);
2783
2979
  return true;
2784
2980
  };
2785
2981
  if (snapToSequentialPoints) {
2786
2982
  const orderedSnapPoints = [...resolvedSnapPoints].sort((first, second) => first.offset - second.offset);
2787
- const orderedOffsets = orderedSnapPoints.map(point => point.offset);
2788
- const currentIndex = closestSnapPointIndex(orderedOffsets, currentOffset);
2789
- let targetSnapPoint = orderedSnapPoints[closestSnapPointIndex(orderedOffsets, targetOffset)];
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
+ }
2790
3005
  const velocityDirection = Math.sign(resolvedDirectionalVelocity);
2791
3006
  const shouldAdvance = dragDirection !== 0 && velocityDirection !== 0 && velocityDirection === dragDirection && Math.abs(resolvedDirectionalVelocity) >= SNAP_VELOCITY_THRESHOLD;
2792
3007
  let effectiveTargetOffset = targetOffset;
@@ -2803,24 +3018,34 @@ const DrawerViewport = /*#__PURE__*/React__namespace.forwardRef(function DrawerV
2803
3018
  return closeFromSnapPoints();
2804
3019
  }
2805
3020
  }
2806
- const closeDistance = Math.abs(effectiveTargetOffset - popupHeight);
3021
+ const closeOffset = popupHeight;
3022
+ const closeDistance = Math.abs(effectiveTargetOffset - closeOffset);
2807
3023
  const snapDistance = Math.abs(effectiveTargetOffset - targetSnapPoint.offset);
2808
3024
  if (closeDistance < snapDistance) {
2809
3025
  return closeFromSnapPoints();
2810
3026
  }
2811
- setActiveSnapPoint(targetSnapPoint.value, snapPointEventDetails);
3027
+ setActiveSnapPoint?.(targetSnapPoint.value, snapPointEventDetails);
2812
3028
  clearSwipeRelease();
2813
3029
  return false;
2814
3030
  }
2815
3031
  if (resolvedDirectionalVelocity >= FAST_SWIPE_VELOCITY && dragDelta > 0) {
2816
3032
  return closeFromSnapPoints();
2817
3033
  }
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)) {
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) {
2821
3046
  return closeFromSnapPoints();
2822
3047
  }
2823
- setActiveSnapPoint(closestSnapPoint.value, snapPointEventDetails);
3048
+ setActiveSnapPoint?.(closestSnapPoint.value, snapPointEventDetails);
2824
3049
  clearSwipeRelease();
2825
3050
  return false;
2826
3051
  },
@@ -2839,7 +3064,7 @@ const DrawerViewport = /*#__PURE__*/React__namespace.forwardRef(function DrawerV
2839
3064
  if (dismissEventDetails.isCanceled) {
2840
3065
  const pendingSnapPoint = pendingSwipeCloseSnapPointRef.current;
2841
3066
  if (pendingSnapPoint !== undefined) {
2842
- setActiveSnapPoint(pendingSnapPoint, useScrollLock.createChangeEventDetails(useScrollLock.swipe, event));
3067
+ setActiveSnapPoint?.(pendingSnapPoint, useScrollLock.createChangeEventDetails(useScrollLock.swipe, event));
2843
3068
  }
2844
3069
  pendingSwipeCloseSnapPointRef.current = undefined;
2845
3070
  resetSwipeRef.current?.();
@@ -2861,7 +3086,7 @@ const DrawerViewport = /*#__PURE__*/React__namespace.forwardRef(function DrawerV
2861
3086
  // Parent rejected: revert animation and restore snap point.
2862
3087
  const pendingSnapPoint = pendingSwipeCloseSnapPointRef.current;
2863
3088
  if (pendingSnapPoint !== undefined) {
2864
- setActiveSnapPoint(pendingSnapPoint, useScrollLock.createChangeEventDetails(useScrollLock.swipe, savedEvent));
3089
+ setActiveSnapPoint?.(pendingSnapPoint, useScrollLock.createChangeEventDetails(useScrollLock.swipe, savedEvent));
2865
3090
  }
2866
3091
  pendingSwipeCloseSnapPointRef.current = undefined;
2867
3092
  clearSwipeRelease();
@@ -2891,18 +3116,42 @@ const DrawerViewport = /*#__PURE__*/React__namespace.forwardRef(function DrawerV
2891
3116
  }
2892
3117
  const resolvedRootElement = rootElement;
2893
3118
  const doc = useScrollLock.ownerDocument(resolvedRootElement);
2894
- function processTouchMove(event, touchState, touch) {
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
+ }
2895
3134
  const drawerAxisDelta = isVerticalScrollAxis ? touch.clientY - touchState.lastY : touch.clientX - touchState.lastX;
2896
3135
 
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
+
2897
3143
  // Avoid blocking pinch zoom or text selection adjustments on iOS Safari.
2898
3144
  if (event.touches.length === 2) {
3145
+ updateTouchScrollPosition(touchState, touch);
2899
3146
  return;
2900
3147
  }
2901
3148
  const allowTouchMove = shouldIgnoreSwipeForTextSelection(doc, resolvedRootElement);
2902
3149
  if (allowTouchMove || !open || !mounted || nestedDrawerOpen) {
3150
+ updateTouchScrollPosition(touchState, touch);
2903
3151
  return;
2904
3152
  }
2905
- if (shouldYieldTouchMove(touchState, event, touch, isVerticalScrollAxis)) {
3153
+ if (preserveNativeCrossAxisScrollOnMove(touchState, touch, isVerticalScrollAxis)) {
3154
+ updateTouchScrollPosition(touchState, touch);
2906
3155
  return;
2907
3156
  }
2908
3157
  const scrollTarget = touchState.scrollTarget;
@@ -2914,21 +3163,27 @@ const DrawerViewport = /*#__PURE__*/React__namespace.forwardRef(function DrawerV
2914
3163
  // move through React re-rasterizes the popup content on every frame.
2915
3164
  event.stopPropagation();
2916
3165
  moveSwipeNative(event, resolvedRootElement);
3166
+ updateTouchScrollPosition(touchState, touch);
2917
3167
  return;
2918
3168
  }
2919
- if (!hasScrollableContentOnAxis(scrollTarget, scrollAxis)) {
3169
+ const hasScrollableContent = hasScrollableContentOnAxis(scrollTarget, scrollAxis);
3170
+ if (!hasScrollableContent) {
2920
3171
  // If the scroll container doesn't overflow on the drawer axis, prevent the window from
2921
3172
  // scrolling instead.
2922
3173
  if (event.cancelable) {
2923
3174
  event.preventDefault();
2924
3175
  }
2925
3176
  event.stopPropagation();
3177
+ updateTouchScrollPosition(touchState, touch);
2926
3178
  return;
2927
3179
  }
2928
- if (drawerAxisDelta !== 0) {
2929
- const canSwipeFromScrollEdge = canSwipeFromScrollEdgeOnMove(scrollTarget, scrollAxis, swipeDirection, drawerAxisDelta);
3180
+ const delta = drawerAxisDelta;
3181
+ if (delta !== 0) {
3182
+ const canSwipeFromScrollEdge = canSwipeFromScrollEdgeOnMove(scrollTarget, scrollAxis, swipeDirection, delta);
2930
3183
  if (!touchState.allowSwipe) {
2931
- if (event.cancelable && canSwipeFromScrollEdge) {
3184
+ if (!event.cancelable) {
3185
+ touchState.allowSwipe = false;
3186
+ } else if (canSwipeFromScrollEdge) {
2932
3187
  touchState.allowSwipe = true;
2933
3188
  event.preventDefault();
2934
3189
  } else {
@@ -2942,22 +3197,6 @@ const DrawerViewport = /*#__PURE__*/React__namespace.forwardRef(function DrawerV
2942
3197
  event.stopPropagation();
2943
3198
  moveSwipeNative(event, resolvedRootElement);
2944
3199
  }
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);
2961
3200
  updateTouchScrollPosition(touchState, touch);
2962
3201
  }
2963
3202
  return useScrollLock.addEventListener(doc, 'touchmove', handleNativeTouchMove, {
@@ -2965,13 +3204,18 @@ const DrawerViewport = /*#__PURE__*/React__namespace.forwardRef(function DrawerV
2965
3204
  capture: true
2966
3205
  });
2967
3206
  }, [mounted, nestedDrawerOpen, open, popupElementState, isVerticalScrollAxis, scrollAxis, swipeDirection, moveSwipeNative, viewportElement, virtualKeyboard]);
2968
- useScrollLock.useIsoLayoutEffect(() => {
3207
+ React__namespace.useEffect(() => {
2969
3208
  if (!snapPointRange || swipe.swiping) {
2970
3209
  return;
2971
3210
  }
2972
- applySwipeProgress(!open || nested ? 0 : snapPointProgress ?? 0, true, false);
3211
+ const resolvedProgress = !open || nested ? 0 : snapPointProgress ?? 0;
3212
+ applySwipeProgress({
3213
+ resolvedProgress,
3214
+ shouldTrackProgress: true,
3215
+ notifyParent: false
3216
+ });
2973
3217
  }, [applySwipeProgress, frontmostHeight, nested, notifyParentSwipeProgressChange, open, snapPointProgress, snapPointRange, swipe.swiping, store, visualStateStore]);
2974
- useScrollLock.useIsoLayoutEffect(() => {
3218
+ React__namespace.useEffect(() => {
2975
3219
  if (!notifyParentSwipeProgressChange) {
2976
3220
  return undefined;
2977
3221
  }
@@ -2982,19 +3226,13 @@ const DrawerViewport = /*#__PURE__*/React__namespace.forwardRef(function DrawerV
2982
3226
  notifyParentSwipeProgressChange(0);
2983
3227
  };
2984
3228
  }, [notifyParentSwipeProgressChange, open]);
2985
- useScrollLock.useIsoLayoutEffect(() => {
3229
+ React__namespace.useEffect(() => {
2986
3230
  if (open) {
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
- }
3231
+ resetSwipe();
2994
3232
  clearSwipeRelease();
2995
3233
  }
2996
- }, [clearSwipeRelease, open, resetSwipe, swipeAreaActiveRef]);
2997
- useScrollLock.useIsoLayoutEffect(() => {
3234
+ }, [clearSwipeRelease, open, resetSwipe]);
3235
+ React__namespace.useEffect(() => {
2998
3236
  const backdropElement = backdropRef.current;
2999
3237
  return () => {
3000
3238
  visualStateStore?.set({
@@ -3019,19 +3257,12 @@ const DrawerViewport = /*#__PURE__*/React__namespace.forwardRef(function DrawerV
3019
3257
  swipeStrength: swipeRelease ?? null,
3020
3258
  setSwipeDismissed
3021
3259
  }), [setSwipeDismissed, swipe.getDragStyles, swipe.swiping, swipeRelease]);
3022
- function resetTouchSwipeState(ignoreSwipe) {
3023
- ignoreTouchSwipeRef.current = ignoreSwipe;
3024
- touchScrollStateRef.current = null;
3025
- }
3026
3260
  function resetTouchTrackingState() {
3027
- resetTouchSwipeState(false);
3261
+ ignoreTouchSwipeRef.current = false;
3262
+ touchScrollStateRef.current = null;
3028
3263
  lastPointerTypeRef.current = '';
3029
3264
  ignoreNextTouchStartFromPenRef.current = false;
3030
3265
  }
3031
- function handlePointerEnd(event) {
3032
- lastPointerTypeRef.current = '';
3033
- return event.pointerType !== 'touch';
3034
- }
3035
3266
  return /*#__PURE__*/jsxRuntime.jsx(DialogViewport, {
3036
3267
  ref: forwardedRef,
3037
3268
  className: className,
@@ -3044,7 +3275,8 @@ const DrawerViewport = /*#__PURE__*/React__namespace.forwardRef(function DrawerV
3044
3275
  if (!open || !mounted || nestedDrawerOpen) {
3045
3276
  return;
3046
3277
  }
3047
- const elementAtPoint = getElementAtPoint(event.currentTarget.getRootNode(), event.clientX, event.clientY);
3278
+ const doc = useScrollLock.ownerDocument(event.currentTarget);
3279
+ const elementAtPoint = getElementAtPoint(doc, event.clientX, event.clientY);
3048
3280
  if (isSwipeIgnoredTarget(elementAtPoint) || isDrawerContentTarget(elementAtPoint)) {
3049
3281
  return;
3050
3282
  }
@@ -3060,24 +3292,34 @@ const DrawerViewport = /*#__PURE__*/React__namespace.forwardRef(function DrawerV
3060
3292
  swipePointerProps.onPointerMove?.(event);
3061
3293
  },
3062
3294
  onPointerUp(event) {
3063
- if (handlePointerEnd(event)) {
3064
- swipePointerProps.onPointerUp?.(event);
3295
+ if (lastPointerTypeRef.current === event.pointerType) {
3296
+ lastPointerTypeRef.current = '';
3297
+ }
3298
+ if (event.pointerType === 'touch') {
3299
+ return;
3065
3300
  }
3301
+ swipePointerProps.onPointerUp?.(event);
3066
3302
  },
3067
3303
  onPointerCancel(event) {
3068
- if (handlePointerEnd(event)) {
3069
- swipePointerProps.onPointerCancel?.(event);
3304
+ if (lastPointerTypeRef.current === event.pointerType) {
3305
+ lastPointerTypeRef.current = '';
3306
+ }
3307
+ if (event.pointerType === 'touch') {
3308
+ return;
3070
3309
  }
3310
+ swipePointerProps.onPointerCancel?.(event);
3071
3311
  },
3072
3312
  onTouchStart(event) {
3073
3313
  const startedFromPenPointerDown = lastPointerTypeRef.current === 'pen' && ignoreNextTouchStartFromPenRef.current;
3074
3314
  if (startedFromPenPointerDown) {
3075
3315
  ignoreNextTouchStartFromPenRef.current = false;
3076
- resetTouchSwipeState(false);
3316
+ ignoreTouchSwipeRef.current = false;
3317
+ touchScrollStateRef.current = null;
3077
3318
  return;
3078
3319
  }
3079
3320
  if (!open || !mounted || nestedDrawerOpen) {
3080
- resetTouchSwipeState(false);
3321
+ ignoreTouchSwipeRef.current = false;
3322
+ touchScrollStateRef.current = null;
3081
3323
  return;
3082
3324
  }
3083
3325
  const touch = event.touches[0];
@@ -3085,25 +3327,32 @@ const DrawerViewport = /*#__PURE__*/React__namespace.forwardRef(function DrawerV
3085
3327
  return;
3086
3328
  }
3087
3329
  if (isReactTouchEventOnRangeInput(event)) {
3088
- resetTouchSwipeState(false);
3330
+ ignoreTouchSwipeRef.current = false;
3331
+ touchScrollStateRef.current = null;
3089
3332
  return;
3090
3333
  }
3091
- const rootElement = event.currentTarget;
3092
- const elementAtPoint = getElementAtPoint(rootElement.getRootNode(), touch.clientX, touch.clientY);
3334
+ const doc = useScrollLock.ownerDocument(event.currentTarget);
3335
+ const elementAtPoint = getElementAtPoint(doc, touch.clientX, touch.clientY);
3336
+ const rootElement = viewportElement ?? popupElementState;
3093
3337
  const eventTarget = useScrollLock.getTarget(event.nativeEvent);
3094
- const target = useScrollLock.isElement(eventTarget) ? eventTarget : rootElement;
3095
- if (!useScrollLock.contains(rootElement, target)) {
3096
- resetTouchSwipeState(true);
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;
3097
3342
  return;
3098
3343
  }
3099
3344
  virtualKeyboard?.onTouchStart(event);
3100
- if (isSwipeIgnoredTarget(elementAtPoint)) {
3101
- resetTouchSwipeState(true);
3345
+ ignoreTouchSwipeRef.current = isSwipeIgnoredTarget(elementAtPoint);
3346
+ if (ignoreTouchSwipeRef.current) {
3347
+ touchScrollStateRef.current = null;
3102
3348
  return;
3103
3349
  }
3104
- ignoreTouchSwipeRef.current = false;
3105
- const scrollTarget = scrollable.findScrollableTouchTarget(target, rootElement, scrollAxis);
3106
- const hasCrossAxisScrollableContent = scrollable.findScrollableTouchTarget(target, rootElement, crossScrollAxis) != null;
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
+ }
3107
3356
  let allowSwipe = null;
3108
3357
  if (scrollTarget) {
3109
3358
  const canSwipeFromEdge = isAtSwipeStartEdge(scrollTarget, scrollAxis, swipeDirection);
@@ -3117,8 +3366,7 @@ const DrawerViewport = /*#__PURE__*/React__namespace.forwardRef(function DrawerV
3117
3366
  scrollTarget,
3118
3367
  hasCrossAxisScrollableContent,
3119
3368
  allowSwipe,
3120
- preserveNativeCrossAxisScroll: false,
3121
- drawerAxisAttributed: false
3369
+ preserveNativeCrossAxisScroll: false
3122
3370
  };
3123
3371
  swipeTouchProps.onTouchStart?.(event);
3124
3372
  },
@@ -3143,8 +3391,24 @@ const DrawerViewport = /*#__PURE__*/React__namespace.forwardRef(function DrawerV
3143
3391
  });
3144
3392
  });
3145
3393
  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
+ }
3146
3403
  function setBackdropSwipingAttribute(backdropElement, swiping) {
3147
- backdropElement?.toggleAttribute(DrawerPopupDataAttributes.swiping, swiping);
3404
+ if (!backdropElement) {
3405
+ return;
3406
+ }
3407
+ if (swiping) {
3408
+ backdropElement.setAttribute(DrawerPopupDataAttributes.swiping, '');
3409
+ return;
3410
+ }
3411
+ backdropElement.removeAttribute(DrawerPopupDataAttributes.swiping);
3148
3412
  }
3149
3413
  function isSwipeIgnoredTarget(target) {
3150
3414
  return Boolean(target?.closest(useScrollLock.BASE_UI_SWIPE_IGNORE_SELECTOR));
@@ -3152,26 +3416,28 @@ function isSwipeIgnoredTarget(target) {
3152
3416
  function isDrawerContentTarget(target) {
3153
3417
  return Boolean(target?.closest(DRAWER_CONTENT_SELECTOR));
3154
3418
  }
3155
- function getBaseSwipeSize(element, direction) {
3156
- return direction === 'left' || direction === 'right' ? element.offsetWidth : element.offsetHeight;
3157
- }
3158
3419
  function getBaseSwipeThreshold(element, direction) {
3159
- return Math.max(getBaseSwipeSize(element, direction) * 0.5, MIN_SWIPE_THRESHOLD);
3420
+ const size = direction === 'left' || direction === 'right' ? element.offsetWidth : element.offsetHeight;
3421
+ return Math.max(size * 0.5, MIN_SWIPE_THRESHOLD);
3160
3422
  }
3161
3423
  function isRangeInput(target, win) {
3162
3424
  return target instanceof win.HTMLInputElement && target.type === 'range';
3163
3425
  }
3164
3426
  function isTextSelectionControl(target) {
3427
+ if (!floatingUi_utils.isElement(target)) {
3428
+ return false;
3429
+ }
3165
3430
  return target.tagName === 'INPUT' || target.tagName === 'TEXTAREA';
3166
3431
  }
3167
3432
  function hasExpandedSelectionWithinTarget(selection, target) {
3168
- const anchorElement = useScrollLock.isElement(selection.anchorNode) ? selection.anchorNode : selection.anchorNode?.parentElement;
3169
- const focusElement = useScrollLock.isElement(selection.focusNode) ? selection.focusNode : selection.focusNode?.parentElement;
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;
3170
3435
  return selection.containsNode(target, true) || useScrollLock.contains(target, anchorElement) || useScrollLock.contains(target, focusElement);
3171
3436
  }
3172
3437
  function shouldIgnoreSwipeForTextSelection(doc, rootElement) {
3173
3438
  const activeEl = useScrollLock.activeElement(doc);
3174
- if (activeEl && useScrollLock.contains(rootElement, activeEl) && isTextSelectionControl(activeEl)) {
3439
+ const activeElementWithinRoot = Boolean(activeEl && useScrollLock.contains(rootElement, activeEl));
3440
+ if (activeElementWithinRoot && isTextSelectionControl(activeEl)) {
3175
3441
  const {
3176
3442
  selectionStart,
3177
3443
  selectionEnd
@@ -3187,60 +3453,38 @@ function shouldIgnoreSwipeForTextSelection(doc, rootElement) {
3187
3453
  return hasExpandedSelectionWithinTarget(selection, rootElement);
3188
3454
  }
3189
3455
  function isEventOnRangeInput(event, win) {
3190
- return event.composedPath().some(pathTarget => isRangeInput(pathTarget, 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);
3191
3461
  }
3192
3462
  function isReactTouchEventOnRangeInput(event) {
3193
- return isEventOnRangeInput(event.nativeEvent, useScrollLock.getWindow(event.currentTarget));
3463
+ return isEventOnRangeInput(event.nativeEvent, floatingUi_utils.getWindow(event.currentTarget));
3194
3464
  }
3195
3465
  function updateTouchScrollPosition(touchState, touch) {
3196
3466
  touchState.lastX = touch.clientX;
3197
3467
  touchState.lastY = touch.clientY;
3198
3468
  }
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) {
3469
+ function preserveNativeCrossAxisScrollOnMove(touchState, touch, isVerticalScrollAxis) {
3206
3470
  if (touchState.preserveNativeCrossAxisScroll) {
3207
3471
  return true;
3208
3472
  }
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) {
3473
+ if (touchState.allowSwipe === true || !touchState.hasCrossAxisScrollableContent) {
3214
3474
  return false;
3215
3475
  }
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
- }
3223
3476
  const drawerAxisGestureDelta = isVerticalScrollAxis ? touch.clientY - touchState.startY : touch.clientX - touchState.startX;
3224
3477
  const crossAxisGestureDelta = isVerticalScrollAxis ? touch.clientX - touchState.startX : touch.clientY - touchState.startY;
3225
3478
  const absDrawerAxisGestureDelta = Math.abs(drawerAxisGestureDelta);
3226
3479
  const absCrossAxisGestureDelta = Math.abs(crossAxisGestureDelta);
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;
3480
+ if (absCrossAxisGestureDelta < 6 || absCrossAxisGestureDelta <= absDrawerAxisGestureDelta + 2) {
3233
3481
  return false;
3234
3482
  }
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.
3483
+ touchState.preserveNativeCrossAxisScroll = true;
3240
3484
  return true;
3241
3485
  }
3242
3486
  function hasScrollableContentOnAxis(scrollTarget, axis) {
3243
- return getScrollMetrics(scrollTarget, axis).max > 0;
3487
+ return axis === 'vertical' ? scrollTarget.scrollHeight > scrollTarget.clientHeight : scrollTarget.scrollWidth > scrollTarget.clientWidth;
3244
3488
  }
3245
3489
  function getScrollMetrics(scrollTarget, axis) {
3246
3490
  if (axis === 'vertical') {
@@ -3257,23 +3501,48 @@ function getScrollMetrics(scrollTarget, axis) {
3257
3501
  };
3258
3502
  }
3259
3503
  function isAtSwipeStartEdge(scrollTarget, axis, direction) {
3260
- const dismissFromStartEdge = shouldDismissFromStartEdge(direction, axis);
3261
3504
  const {
3262
3505
  offset,
3263
3506
  max
3264
3507
  } = getScrollMetrics(scrollTarget, axis);
3508
+ const dismissFromStartEdge = shouldDismissFromStartEdge(direction, axis);
3509
+ if (dismissFromStartEdge === null) {
3510
+ return false;
3511
+ }
3265
3512
  return dismissFromStartEdge ? offset <= 0 : offset >= max;
3266
3513
  }
3267
3514
  function canSwipeFromScrollEdgeOnMove(scrollTarget, axis, direction, delta) {
3515
+ const {
3516
+ offset,
3517
+ max
3518
+ } = getScrollMetrics(scrollTarget, axis);
3268
3519
  const dismissFromStartEdge = shouldDismissFromStartEdge(direction, axis);
3520
+ if (dismissFromStartEdge === null) {
3521
+ return false;
3522
+ }
3269
3523
  const movingTowardDismiss = dismissFromStartEdge ? delta > 0 : delta < 0;
3270
3524
  if (!movingTowardDismiss) {
3271
3525
  return false;
3272
3526
  }
3273
- return isAtSwipeStartEdge(scrollTarget, axis, direction);
3527
+ return dismissFromStartEdge ? offset <= 0 : offset >= max;
3274
3528
  }
3275
3529
  function shouldDismissFromStartEdge(direction, axis) {
3276
- return axis === 'vertical' ? direction === 'down' : direction === 'right';
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;
3277
3546
  }
3278
3547
 
3279
3548
  const ScrollAreaRootContext = /*#__PURE__*/React__namespace.createContext(undefined);
@@ -3286,6 +3555,20 @@ function useScrollAreaRootContext() {
3286
3555
  return context;
3287
3556
  }
3288
3557
 
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
+
3289
3572
  const SCROLL_TIMEOUT = 500;
3290
3573
  const MIN_THUMB_SIZE = 16;
3291
3574
 
@@ -3294,28 +3577,108 @@ function getOffset(element, prop, axis) {
3294
3577
  return 0;
3295
3578
  }
3296
3579
  const styles = getComputedStyle(element);
3297
- const key = `${prop}${axis === 'x' ? 'Inline' : 'Block'}`;
3298
- const start = parseFloat(styles[`${key}Start`]);
3580
+ const propAxis = axis === 'x' ? 'Inline' : 'Block';
3299
3581
 
3300
3582
  // Safari misreports `marginInlineEnd` in RTL.
3301
3583
  // We have to assume the start/end values are symmetrical, which is likely.
3302
3584
  if (axis === 'x' && prop === 'margin') {
3303
- return start * 2;
3585
+ return parseFloat(styles[`${prop}InlineStart`]) * 2;
3304
3586
  }
3305
- return start + parseFloat(styles[`${key}End`]);
3587
+ return parseFloat(styles[`${prop}${propAxis}Start`]) + parseFloat(styles[`${prop}${propAxis}End`]);
3306
3588
  }
3307
3589
 
3308
- // Data-attribute strings inlined so `ScrollAreaRootDataAttributes` tree-shakes out.
3309
- const attr = name => value => value ? {
3310
- [name]: ''
3311
- } : null;
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
+
3312
3663
  const scrollAreaStateAttributesMapping = {
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'),
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,
3319
3682
  cornerHidden: () => null
3320
3683
  };
3321
3684
 
@@ -3381,145 +3744,99 @@ const ScrollAreaRoot = /*#__PURE__*/React__namespace.forwardRef(function ScrollA
3381
3744
  const thumbYRef = React__namespace.useRef(null);
3382
3745
  const thumbXRef = React__namespace.useRef(null);
3383
3746
  const cornerRef = React__namespace.useRef(null);
3384
- const activePointerIdRef = React__namespace.useRef(null);
3747
+ const thumbDraggingRef = React__namespace.useRef(false);
3385
3748
  const startYRef = React__namespace.useRef(0);
3386
3749
  const startXRef = React__namespace.useRef(0);
3387
3750
  const startScrollTopRef = React__namespace.useRef(0);
3388
3751
  const startScrollLeftRef = React__namespace.useRef(0);
3389
3752
  const currentOrientationRef = React__namespace.useRef('vertical');
3390
3753
  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
- }
3400
3754
  const handleScroll = useScrollLock.useStableCallback(scrollPosition => {
3401
3755
  const offsetX = scrollPosition.x - scrollPositionRef.current.x;
3402
3756
  const offsetY = scrollPosition.y - scrollPositionRef.current.y;
3403
3757
  scrollPositionRef.current = scrollPosition;
3404
3758
  if (offsetY !== 0) {
3405
- startScrolling(true);
3759
+ setScrollingY(true);
3760
+ scrollYTimeout.start(SCROLL_TIMEOUT, () => {
3761
+ setScrollingY(false);
3762
+ });
3406
3763
  }
3407
3764
  if (offsetX !== 0) {
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';
3765
+ setScrollingX(true);
3766
+ scrollXTimeout.start(SCROLL_TIMEOUT, () => {
3767
+ setScrollingX(false);
3768
+ });
3423
3769
  }
3424
3770
  });
3425
3771
  const handlePointerDown = useScrollLock.useStableCallback(event => {
3426
3772
  if (event.button !== 0) {
3427
3773
  return;
3428
3774
  }
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;
3775
+ thumbDraggingRef.current = true;
3440
3776
  startYRef.current = event.clientY;
3441
3777
  startXRef.current = event.clientX;
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();
3451
- }
3452
- const thumb = currentOrientationRef.current === 'vertical' ? thumbYRef.current : thumbXRef.current;
3453
- thumb?.setPointerCapture(event.pointerId);
3454
- });
3455
- const handlePointerUp = useScrollLock.useStableCallback(event => {
3456
- if (event.pointerId !== activePointerIdRef.current) {
3457
- return;
3778
+ currentOrientationRef.current = event.currentTarget.getAttribute(ScrollAreaScrollbarDataAttributes.orientation);
3779
+ if (viewportRef.current) {
3780
+ startScrollTopRef.current = viewportRef.current.scrollTop;
3781
+ startScrollLeftRef.current = viewportRef.current.scrollLeft;
3458
3782
  }
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;
3467
- }
3468
- savedSnapTypeRef.current = null;
3783
+ if (thumbYRef.current && currentOrientationRef.current === 'vertical') {
3784
+ thumbYRef.current.setPointerCapture(event.pointerId);
3469
3785
  }
3470
- const thumb = currentOrientationRef.current === 'vertical' ? thumbYRef.current : thumbXRef.current;
3471
- // `pointercancel` releases capture implicitly, so guard against releasing a
3472
- // capture we no longer hold (which would throw).
3473
- if (thumb?.hasPointerCapture(event.pointerId)) {
3474
- thumb.releasePointerCapture(event.pointerId);
3786
+ if (thumbXRef.current && currentOrientationRef.current === 'horizontal') {
3787
+ thumbXRef.current.setPointerCapture(event.pointerId);
3475
3788
  }
3476
3789
  });
3477
3790
  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);
3791
+ if (!thumbDraggingRef.current) {
3489
3792
  return;
3490
3793
  }
3491
- const viewportEl = viewportRef.current;
3492
- if (!viewportEl) {
3493
- return;
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
+ });
3826
+ }
3494
3827
  }
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;
3828
+ });
3829
+ const handlePointerUp = useScrollLock.useStableCallback(event => {
3830
+ thumbDraggingRef.current = false;
3831
+
3832
+ // `pointercancel` releases capture implicitly, so guard against releasing a
3833
+ // 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);
3500
3836
  }
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;
3837
+ if (thumbXRef.current && currentOrientationRef.current === 'horizontal' && thumbXRef.current.hasPointerCapture(event.pointerId)) {
3838
+ thumbXRef.current.releasePointerCapture(event.pointerId);
3520
3839
  }
3521
- event.preventDefault();
3522
- startScrolling(vertical);
3523
3840
  });
3524
3841
  function handleTouchModalityChange(event) {
3525
3842
  setTouchModality(event.pointerType === 'touch');
@@ -3551,8 +3868,8 @@ const ScrollAreaRoot = /*#__PURE__*/React__namespace.forwardRef(function ScrollA
3551
3868
  },
3552
3869
  style: {
3553
3870
  position: 'relative',
3554
- ['--scroll-area-corner-height']: `${cornerSize.height}px`,
3555
- ['--scroll-area-corner-width']: `${cornerSize.width}px`
3871
+ [ScrollAreaRootCssVars.scrollAreaCornerHeight]: `${cornerSize.height}px`,
3872
+ [ScrollAreaRootCssVars.scrollAreaCornerWidth]: `${cornerSize.width}px`
3556
3873
  }
3557
3874
  };
3558
3875
  const element = useRenderElement.useRenderElement('div', componentProps, {
@@ -3566,7 +3883,6 @@ const ScrollAreaRoot = /*#__PURE__*/React__namespace.forwardRef(function ScrollA
3566
3883
  handlePointerMove,
3567
3884
  handlePointerUp,
3568
3885
  handleScroll,
3569
- disableViewportSnap,
3570
3886
  cornerSize,
3571
3887
  setCornerSize,
3572
3888
  thumbSize,
@@ -3576,10 +3892,13 @@ const ScrollAreaRoot = /*#__PURE__*/React__namespace.forwardRef(function ScrollA
3576
3892
  touchModality,
3577
3893
  cornerRef,
3578
3894
  scrollingX,
3895
+ setScrollingX,
3579
3896
  scrollingY,
3897
+ setScrollingY,
3580
3898
  hovering,
3581
3899
  setHovering,
3582
3900
  viewportRef,
3901
+ rootRef,
3583
3902
  scrollbarYRef,
3584
3903
  scrollbarXRef,
3585
3904
  thumbYRef,
@@ -3596,7 +3915,7 @@ const ScrollAreaRoot = /*#__PURE__*/React__namespace.forwardRef(function ScrollA
3596
3915
  yStart,
3597
3916
  yEnd
3598
3917
  }
3599
- }), [handlePointerDown, handlePointerMove, handlePointerUp, handleScroll, disableViewportSnap, cornerSize, thumbSize, hasMeasuredScrollbar, touchModality, scrollingX, scrollingY, hovering, rootId, hiddenState, overflowEdges, state, xStart, xEnd, yStart, yEnd]);
3918
+ }), [handlePointerDown, handlePointerMove, handlePointerUp, handleScroll, cornerSize, thumbSize, hasMeasuredScrollbar, touchModality, scrollingX, setScrollingX, scrollingY, setScrollingY, hovering, setHovering, rootId, hiddenState, overflowEdges, state, xStart, xEnd, yStart, yEnd]);
3600
3919
  return /*#__PURE__*/jsxRuntime.jsxs(ScrollAreaRootContext.Provider, {
3601
3920
  value: contextValue,
3602
3921
  children: [!disableStyleElements && scrollEdges.styleDisableScrollbar.getElement(nonce), element]
@@ -3604,17 +3923,20 @@ const ScrollAreaRoot = /*#__PURE__*/React__namespace.forwardRef(function ScrollA
3604
3923
  });
3605
3924
  if (process.env.NODE_ENV !== "production") ScrollAreaRoot.displayName = "ScrollAreaRoot";
3606
3925
  function normalizeOverflowEdgeThreshold(threshold) {
3607
- const thresholds = typeof threshold === 'number' ? {
3608
- xStart: threshold,
3609
- xEnd: threshold,
3610
- yStart: threshold,
3611
- yEnd: threshold
3612
- } : 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
+ }
3613
3935
  return {
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)
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)
3618
3940
  };
3619
3941
  }
3620
3942
 
@@ -3628,10 +3950,30 @@ function useScrollAreaViewportContext() {
3628
3950
  return context;
3629
3951
  }
3630
3952
 
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'];
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
+ }({});
3632
3976
 
3633
- // Module-level flag to ensure we only register the CSS properties once,
3634
- // regardless of how many Scroll Area components are mounted.
3635
3977
  let scrollAreaOverflowVarsRegistered = false;
3636
3978
 
3637
3979
  /**
@@ -3650,7 +3992,7 @@ function removeCSSVariableInheritance() {
3650
3992
  return;
3651
3993
  }
3652
3994
  if (typeof CSS !== 'undefined' && 'registerProperty' in CSS) {
3653
- OVERFLOW_EDGE_VARS.forEach(name => {
3995
+ [ScrollAreaViewportCssVars.scrollAreaOverflowXStart, ScrollAreaViewportCssVars.scrollAreaOverflowXEnd, ScrollAreaViewportCssVars.scrollAreaOverflowYStart, ScrollAreaViewportCssVars.scrollAreaOverflowYEnd].forEach(name => {
3654
3996
  try {
3655
3997
  CSS.registerProperty({
3656
3998
  name,
@@ -3694,11 +4036,12 @@ const ScrollAreaViewport = /*#__PURE__*/React__namespace.forwardRef(function Scr
3694
4036
  hiddenState,
3695
4037
  setHasMeasuredScrollbar,
3696
4038
  handleScroll,
3697
- touchModality,
3698
4039
  setHovering,
3699
4040
  setOverflowEdges,
4041
+ overflowEdges,
3700
4042
  overflowEdgeThreshold,
3701
- viewportState
4043
+ scrollingX,
4044
+ scrollingY
3702
4045
  } = useScrollAreaRootContext();
3703
4046
  const direction = useScrollLock.useDirection();
3704
4047
  const programmaticScrollRef = React__namespace.useRef(true);
@@ -3743,12 +4086,18 @@ const ScrollAreaViewport = /*#__PURE__*/React__namespace.forwardRef(function Scr
3743
4086
  let scrollLeftFromStart = 0;
3744
4087
  let scrollLeftFromEnd = 0;
3745
4088
  if (!scrollbarXHidden) {
3746
- // `normalizeScrollOffset` clamps internally.
3747
- scrollLeftFromStart = scrollEdges.normalizeScrollOffset(direction === 'rtl' ? -scrollLeft : scrollLeft, maxScrollLeft);
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);
3748
4096
  scrollLeftFromEnd = maxScrollLeft - scrollLeftFromStart;
3749
4097
  }
3750
- const scrollTopFromStart = scrollbarYHidden ? 0 : scrollEdges.normalizeScrollOffset(scrollTop, maxScrollTop);
3751
- const scrollTopFromEnd = scrollbarYHidden ? 0 : maxScrollTop - scrollTopFromStart;
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;
3752
4101
  const nextWidth = scrollbarXHidden ? 0 : viewportWidth;
3753
4102
  const nextHeight = scrollbarYHidden ? 0 : viewportHeight;
3754
4103
  let nextCornerWidth = 0;
@@ -3773,53 +4122,76 @@ const ScrollAreaViewport = /*#__PURE__*/React__namespace.forwardRef(function Scr
3773
4122
  const maxNextHeight = scrollbarYEl ? Math.min(scrollbarYEl.offsetHeight - cornerHeightOffset, idealNextHeight) : idealNextHeight;
3774
4123
  const clampedNextWidth = Math.max(MIN_THUMB_SIZE, maxNextWidth * ratioX);
3775
4124
  const clampedNextHeight = Math.max(MIN_THUMB_SIZE, maxNextHeight * ratioY);
3776
- setThumbSize(prevSize => pickState(prevSize, {
3777
- width: clampedNextWidth,
3778
- height: clampedNextHeight
3779
- }));
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
+ });
3780
4134
 
3781
4135
  // Handle Y (vertical) scroll
3782
4136
  if (scrollbarYEl && thumbYEl) {
3783
4137
  const maxThumbOffsetY = scrollbarYEl.offsetHeight - clampedNextHeight - scrollbarYOffset - thumbYOffset;
3784
- const thumbOffsetY = applyOverscrollThumb(thumbYEl, '--scroll-area-thumb-height', scrollTop, maxScrollTop, scrollableContentHeight, clampedNextHeight, maxThumbOffsetY);
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));
3785
4144
  thumbYEl.style.transform = `translate3d(0,${thumbOffsetY}px,0)`;
3786
4145
  }
3787
4146
 
3788
4147
  // Handle X (horizontal) scroll
3789
4148
  if (scrollbarXEl && thumbXEl) {
3790
4149
  const maxThumbOffsetX = scrollbarXEl.offsetWidth - clampedNextWidth - scrollbarXOffset - thumbXOffset;
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
- });
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
+ }
3801
4162
  if (cornerEl) {
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));
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));
3812
4176
  const nextOverflowEdges = {
3813
4177
  xStart: !scrollbarXHidden && scrollLeftFromStart > overflowEdgeThreshold.xStart,
3814
4178
  xEnd: !scrollbarXHidden && scrollLeftFromEnd > overflowEdgeThreshold.xEnd,
3815
4179
  yStart: !scrollbarYHidden && scrollTopFromStart > overflowEdgeThreshold.yStart,
3816
4180
  yEnd: !scrollbarYHidden && scrollTopFromEnd > overflowEdgeThreshold.yEnd
3817
4181
  };
3818
- setOverflowEdges(prev => pickState(prev, nextOverflowEdges));
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
+ });
3819
4188
  });
3820
4189
  useScrollLock.useIsoLayoutEffect(() => {
4190
+ if (!viewportRef.current) {
4191
+ return;
4192
+ }
3821
4193
  removeCSSVariableInheritance();
3822
- }, []);
4194
+ }, [viewportRef]);
3823
4195
  useScrollLock.useIsoLayoutEffect(() => {
3824
4196
  // Wait for scrollbar and thumb refs after hidden-state toggles, refresh math on direction
3825
4197
  // flips, and re-evaluate overflow edges when the threshold changes.
@@ -3862,10 +4234,6 @@ const ScrollAreaViewport = /*#__PURE__*/React__namespace.forwardRef(function Scr
3862
4234
  if (animations.length === 0) {
3863
4235
  return;
3864
4236
  }
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.
3869
4237
  Promise.allSettled(animations.map(animation => animation.finished)).then(computeThumbPosition).catch(() => {});
3870
4238
  });
3871
4239
  return () => {
@@ -3893,13 +4261,7 @@ const ScrollAreaViewport = /*#__PURE__*/React__namespace.forwardRef(function Scr
3893
4261
  return;
3894
4262
  }
3895
4263
  computeThumbPosition();
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) {
4264
+ if (!programmaticScrollRef.current) {
3903
4265
  handleScroll({
3904
4266
  x: viewportRef.current.scrollLeft,
3905
4267
  y: viewportRef.current.scrollTop
@@ -3917,10 +4279,21 @@ const ScrollAreaViewport = /*#__PURE__*/React__namespace.forwardRef(function Scr
3917
4279
  });
3918
4280
  },
3919
4281
  onWheel: handleUserInteraction,
4282
+ onTouchMove: handleUserInteraction,
3920
4283
  onPointerMove: handleUserInteraction,
3921
4284
  onPointerEnter: handleUserInteraction,
3922
4285
  onKeyDown: handleUserInteraction
3923
4286
  };
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]);
3924
4297
  const element = useRenderElement.useRenderElement('div', componentProps, {
3925
4298
  ref: [forwardedRef, viewportRef],
3926
4299
  state: viewportState,
@@ -3945,40 +4318,16 @@ function getHiddenState(viewport) {
3945
4318
  corner: y || x
3946
4319
  };
3947
4320
  }
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
- }
4321
+ function mergeHiddenState(prevState, nextState) {
4322
+ if (prevState.y === nextState.y && prevState.x === nextState.x && prevState.corner === nextState.corner) {
4323
+ return prevState;
3958
4324
  }
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);
4325
+ return nextState;
3979
4326
  }
3980
4327
 
3981
4328
  exports.DialogPortal = DialogPortal;
4329
+ exports.DialogRoot = DialogRoot;
4330
+ exports.DialogStore = DialogStore;
3982
4331
  exports.DialogTrigger = DialogTrigger;
3983
4332
  exports.DialogViewport = DialogViewport;
3984
4333
  exports.DrawerBackdrop = DrawerBackdrop;
@@ -3993,12 +4342,11 @@ exports.DrawerRoot = DrawerRoot;
3993
4342
  exports.DrawerTrigger = DrawerTrigger;
3994
4343
  exports.DrawerViewport = DrawerViewport;
3995
4344
  exports.DrawerVirtualKeyboardContext = DrawerVirtualKeyboardContext;
3996
- exports.NullStore = NullStore;
3997
4345
  exports.ScrollAreaRoot = ScrollAreaRoot;
4346
+ exports.ScrollAreaRootCssVars = ScrollAreaRootCssVars;
3998
4347
  exports.ScrollAreaViewport = ScrollAreaViewport;
3999
4348
  exports.createInitialPopupStoreState = createInitialPopupStoreState;
4000
- exports.createNullDialogStore = createNullDialogStore;
4001
- exports.dialogStateAttributesMapping = dialogStateAttributesMapping;
4349
+ exports.findScrollableTouchTarget = findScrollableTouchTarget;
4002
4350
  exports.getDisplacement = getDisplacement;
4003
4351
  exports.getElementAtPoint = getElementAtPoint;
4004
4352
  exports.getElementTransform = getElementTransform;
@@ -4009,8 +4357,6 @@ exports.useDialogPortalContext = useDialogPortalContext;
4009
4357
  exports.useDialogRootContext = useDialogRootContext;
4010
4358
  exports.useDrawerProviderContext = useDrawerProviderContext;
4011
4359
  exports.useDrawerRootContext = useDrawerRootContext;
4012
- exports.usePopupHandleStore = usePopupHandleStore;
4013
- exports.useRenderDialogRoot = useRenderDialogRoot;
4014
4360
  exports.useScrollAreaRootContext = useScrollAreaRootContext;
4015
4361
  exports.useScrollAreaViewportContext = useScrollAreaViewportContext;
4016
4362
  exports.useSwipeDismiss = useSwipeDismiss;