@jobber/components 9.5.3 → 9.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/Combobox.d.ts +1 -0
  2. package/Combobox.js +17 -0
  3. package/dist/Autocomplete/index.cjs +0 -2
  4. package/dist/Autocomplete/index.mjs +0 -2
  5. package/dist/Autocomplete-cjs.js +4 -5
  6. package/dist/Autocomplete-es.js +1 -2
  7. package/dist/Card/index.cjs +2 -2
  8. package/dist/Card/index.mjs +2 -2
  9. package/dist/Chip/index.cjs +0 -2
  10. package/dist/Chip/index.mjs +0 -2
  11. package/dist/Chips/InternalChipDismissible/hooks/index.cjs +0 -2
  12. package/dist/Chips/InternalChipDismissible/hooks/index.mjs +0 -2
  13. package/dist/Chips/InternalChipDismissible/index.cjs +0 -2
  14. package/dist/Chips/InternalChipDismissible/index.mjs +0 -2
  15. package/dist/Chips/index.cjs +0 -2
  16. package/dist/Chips/index.mjs +0 -2
  17. package/dist/Combobox/Combobox.contentSlots.d.ts +5 -0
  18. package/dist/Combobox/Combobox.context.d.ts +34 -0
  19. package/dist/Combobox/Combobox.d.ts +69 -0
  20. package/dist/Combobox/Combobox.footer.d.ts +7 -0
  21. package/dist/Combobox/Combobox.items.d.ts +25 -0
  22. package/dist/Combobox/Combobox.responsive.d.ts +54 -0
  23. package/dist/Combobox/Combobox.selectAll.d.ts +4 -0
  24. package/dist/Combobox/Combobox.slotFactory.d.ts +7 -0
  25. package/dist/Combobox/Combobox.testUtils.d.ts +12 -0
  26. package/dist/Combobox/Combobox.types.d.ts +166 -0
  27. package/dist/Combobox/index.cjs +35 -0
  28. package/dist/Combobox/index.d.ts +2 -0
  29. package/dist/Combobox/index.mjs +29 -0
  30. package/dist/Combobox-cjs.js +675 -0
  31. package/dist/Combobox-es.js +673 -0
  32. package/dist/ComboboxPrimitive-cjs.js +88 -47
  33. package/dist/ComboboxPrimitive-es.js +39 -23
  34. package/dist/{ComboboxChipRemove-cjs.js → ComboboxSeparator-cjs.js} +501 -455
  35. package/dist/{ComboboxChipRemove-es.js → ComboboxSeparator-es.js} +500 -456
  36. package/dist/ConfirmationModal/index.cjs +0 -2
  37. package/dist/ConfirmationModal/index.mjs +0 -2
  38. package/dist/ContentBlock/index.cjs +1 -0
  39. package/dist/ContentBlock/index.mjs +1 -0
  40. package/dist/ContentBlock-cjs.js +2 -18
  41. package/dist/ContentBlock-es.js +1 -17
  42. package/dist/DataDump/index.cjs +2 -2
  43. package/dist/DataDump/index.mjs +2 -2
  44. package/dist/DataList/components/DataListActions/index.cjs +0 -2
  45. package/dist/DataList/components/DataListActions/index.mjs +0 -2
  46. package/dist/DataList/components/DataListBulkActions/index.cjs +0 -2
  47. package/dist/DataList/components/DataListBulkActions/index.mjs +0 -2
  48. package/dist/DataList/components/DataListFilters/components/DataListSort/index.cjs +0 -2
  49. package/dist/DataList/components/DataListFilters/components/DataListSort/index.mjs +0 -2
  50. package/dist/DataList/components/DataListFilters/index.cjs +0 -2
  51. package/dist/DataList/components/DataListFilters/index.mjs +0 -2
  52. package/dist/DataList/components/DataListHeader/index.cjs +0 -2
  53. package/dist/DataList/components/DataListHeader/index.mjs +0 -2
  54. package/dist/DataList/components/DataListItem/index.cjs +0 -2
  55. package/dist/DataList/components/DataListItem/index.mjs +0 -2
  56. package/dist/DataList/components/DataListItemActions/index.cjs +0 -2
  57. package/dist/DataList/components/DataListItemActions/index.mjs +0 -2
  58. package/dist/DataList/components/DataListItemActionsOverflow/index.cjs +0 -2
  59. package/dist/DataList/components/DataListItemActionsOverflow/index.mjs +0 -2
  60. package/dist/DataList/components/DataListItems/index.cjs +0 -2
  61. package/dist/DataList/components/DataListItems/index.mjs +0 -2
  62. package/dist/DataList/components/DataListLayout/index.cjs +0 -2
  63. package/dist/DataList/components/DataListLayout/index.mjs +0 -2
  64. package/dist/DataList/components/DataListLayoutActions/index.cjs +0 -2
  65. package/dist/DataList/components/DataListLayoutActions/index.mjs +0 -2
  66. package/dist/DataList/index.cjs +0 -2
  67. package/dist/DataList/index.mjs +0 -2
  68. package/dist/DatePicker/index.cjs +0 -2
  69. package/dist/DatePicker/index.mjs +0 -2
  70. package/dist/DatePicker-cjs.js +4 -5
  71. package/dist/DatePicker-es.js +1 -2
  72. package/dist/Dialog/index.cjs +2 -2
  73. package/dist/Dialog/index.mjs +2 -2
  74. package/dist/DrawerDescription-cjs.js +268 -99
  75. package/dist/DrawerDescription-es.js +270 -103
  76. package/dist/DrawerTitle-cjs.js +2 -6
  77. package/dist/DrawerTitle-es.js +3 -7
  78. package/dist/FieldDescription-cjs.js +184 -181
  79. package/dist/FieldDescription-es.js +187 -184
  80. package/dist/FilterPicker/components/FilterPickerActivator/index.cjs +0 -2
  81. package/dist/FilterPicker/components/FilterPickerActivator/index.mjs +0 -2
  82. package/dist/FilterPicker/components/FilterPickerContent/index.cjs +0 -2
  83. package/dist/FilterPicker/components/FilterPickerContent/index.mjs +0 -2
  84. package/dist/FilterPicker/components/FilterPickerTrigger/index.cjs +0 -2
  85. package/dist/FilterPicker/components/FilterPickerTrigger/index.mjs +0 -2
  86. package/dist/FilterPicker/index.cjs +0 -2
  87. package/dist/FilterPicker/index.mjs +0 -2
  88. package/dist/FilterPickerContent-cjs.js +4 -5
  89. package/dist/FilterPickerContent-es.js +1 -2
  90. package/dist/FormatFile/index.cjs +0 -2
  91. package/dist/FormatFile/index.mjs +0 -2
  92. package/dist/Gallery/index.cjs +0 -2
  93. package/dist/Gallery/index.mjs +0 -2
  94. package/dist/InputDate/index.cjs +0 -2
  95. package/dist/InputDate/index.mjs +0 -2
  96. package/dist/InputNumber/index.cjs +2 -2
  97. package/dist/InputNumber/index.mjs +2 -2
  98. package/dist/InputNumber/types.d.ts +5 -0
  99. package/dist/InputNumber-cjs.js +2 -2
  100. package/dist/InputNumber-es.js +2 -2
  101. package/dist/LightBox/index.cjs +0 -2
  102. package/dist/LightBox/index.mjs +0 -2
  103. package/dist/Menu/index.cjs +2 -2
  104. package/dist/Menu/index.mjs +2 -2
  105. package/dist/Menu-cjs.js +4 -5
  106. package/dist/Menu-es.js +1 -2
  107. package/dist/MenuSubmenuTrigger-cjs.js +206 -164
  108. package/dist/MenuSubmenuTrigger-es.js +194 -152
  109. package/dist/Modal/index.cjs +0 -2
  110. package/dist/Modal/index.mjs +1 -3
  111. package/dist/NumberFieldInput-cjs.js +187 -308
  112. package/dist/NumberFieldInput-es.js +187 -308
  113. package/dist/Page/index.cjs +2 -2
  114. package/dist/Page/index.mjs +2 -2
  115. package/dist/Popover/index.cjs +0 -2
  116. package/dist/Popover/index.mjs +0 -2
  117. package/dist/Popover-cjs.js +7 -8
  118. package/dist/Popover-es.js +1 -2
  119. package/dist/ScrollAreaViewport-cjs.js +747 -1093
  120. package/dist/ScrollAreaViewport-es.js +734 -1081
  121. package/dist/Select/index.cjs +2 -2
  122. package/dist/Select/index.mjs +2 -2
  123. package/dist/SelectGroupLabel-cjs.js +207 -306
  124. package/dist/SelectGroupLabel-es.js +203 -302
  125. package/dist/SelectPrimitive-cjs.js +11 -17
  126. package/dist/SelectPrimitive-es.js +1 -7
  127. package/dist/Tooltip/index.cjs +0 -2
  128. package/dist/Tooltip/index.mjs +0 -2
  129. package/dist/Tooltip-cjs.js +6 -7
  130. package/dist/Tooltip-es.js +1 -2
  131. package/dist/buttonRenderAdapter-cjs.js +2 -1
  132. package/dist/buttonRenderAdapter-es.js +2 -1
  133. package/dist/dialogReturnFocus-cjs.js +1 -1
  134. package/dist/dialogReturnFocus-es.js +2 -2
  135. package/dist/docs/InputNumber/InputNumber.md +2 -2
  136. package/dist/docs/SelectPrimitive/SelectPrimitive.md +1 -1
  137. package/dist/floating-ui.react-cjs.js +2557 -185
  138. package/dist/floating-ui.react-es.js +2512 -149
  139. package/dist/getMappedBreakpointWidth-cjs.js +21 -0
  140. package/dist/getMappedBreakpointWidth-es.js +18 -0
  141. package/dist/index.cjs +15 -10
  142. package/dist/index.d.mts +1 -0
  143. package/dist/index.d.ts +1 -0
  144. package/dist/index.mjs +14 -10
  145. package/dist/mergeClassName-cjs.js +12 -0
  146. package/dist/mergeClassName-es.js +10 -0
  147. package/dist/primitives/BottomSheet/index.cjs +1 -1
  148. package/dist/primitives/BottomSheet/index.mjs +1 -1
  149. package/dist/primitives/ComboboxPrimitive/ComboboxPrimitive.d.ts +2 -2
  150. package/dist/primitives/ComboboxPrimitive/index.cjs +4 -5
  151. package/dist/primitives/ComboboxPrimitive/index.mjs +4 -5
  152. package/dist/primitives/SelectPrimitive/index.cjs +1 -2
  153. package/dist/primitives/SelectPrimitive/index.mjs +1 -2
  154. package/dist/primitives/index.cjs +3 -3
  155. package/dist/primitives/index.mjs +3 -3
  156. package/dist/scrollEdges-cjs.js +1 -11
  157. package/dist/scrollEdges-es.js +1 -11
  158. package/dist/scrollable-cjs.js +58 -0
  159. package/dist/scrollable-es.js +54 -0
  160. package/dist/styles.css +759 -42
  161. package/dist/unstyledPrimitives/index.cjs +720 -478
  162. package/dist/unstyledPrimitives/index.mjs +663 -421
  163. package/dist/useCompositeListItem-cjs.js +2108 -361
  164. package/dist/useCompositeListItem-es.js +2076 -331
  165. package/dist/useLabel-cjs.js +2 -3
  166. package/dist/useLabel-es.js +2 -3
  167. package/dist/useRenderElement-cjs.js +9 -7
  168. package/dist/useRenderElement-es.js +9 -7
  169. package/dist/useScrollLock-cjs.js +819 -344
  170. package/dist/useScrollLock-es.js +732 -296
  171. package/dist/useScrollToActive-cjs.js +4 -5
  172. package/dist/useScrollToActive-es.js +1 -2
  173. package/dist/utils/meta/meta.json +43 -1
  174. package/package.json +8 -3
  175. package/dist/floating-ui.react-dom-cjs.js +0 -2103
  176. package/dist/floating-ui.react-dom-es.js +0 -2072
  177. package/dist/floating-ui.utils-cjs.js +0 -342
  178. package/dist/floating-ui.utils-es.js +0 -300
@@ -1,10 +1,10 @@
1
1
  import * as React from 'react';
2
- import { E as EMPTY_OBJECT, f as formatErrorMessage, u as useRenderElement, m as mergeProps } from './useRenderElement-es.js';
3
- import { ac as FloatingRootStore, ad as PopupTriggerMap, N as createSelector, a0 as FloatingPortal, a1 as InternalBackdrop, a2 as inertValue, b9 as usePopupRootSync, aS as useImplicitActiveTrigger, aT as useOpenStateTransitions, d as createChangeEventDetails, j as imperativeAction, R as useDismiss, g as getTarget, _ as contains, av as useScrollLock, aY as usePopupInteractionProps, aN as ReactStore, aV as setPopupOpenState, ba as usePopupStore, U as useOnFirstRender, C as CommonPopupDataAttributes, p as popupStateMapping, t as transitionStatusMapping, u as useBaseUiId, b4 as useTriggerDataForwarding, c as useButton, Q as useClick, bb as useOpenMethodTriggerProps, bc as CLICK_TRIGGER_IDENTIFIER, b8 as triggerOpenStateMapping, b as useStableCallback, o as ownerDocument, a as useIsoLayoutEffect, S as SafeReact, l as error, f as useOpenChangeComplete, F as FOCUSABLE_POPUP_PROPS, i as FloatingFocusManager, n as useControlled, v as none, aa as useId, bd as android, s as addEventListener, be as closeWatcher, a5 as useAnimationFrame, bf as BASE_UI_SWIPE_IGNORE_SELECTOR, D as activeElement, bg as TransitionStatusDataAttributes, bh as swipe, k as useTimeout, a4 as useDirection, a6 as webkit } from './useScrollLock-es.js';
2
+ import { E as EMPTY_OBJECT, N as NOOP, f as formatErrorMessage, u as useRenderElement, m as mergeProps } from './useRenderElement-es.js';
3
+ import { aK as FloatingRootStore, aL as PopupTriggerMap, b3 as shimExports, a6 as popupStateMapping, t as transitionStatusMapping, a2 as FloatingPortal, a3 as InternalBackdrop, a4 as inertValue, W as useDismiss, e as getTarget, a0 as contains, b2 as useScrollLock, c as useIsoLayoutEffect, b4 as usePopupInteractionProps, X as ReactStore, b5 as setPopupOpenState, b6 as usePopupRootStore, b7 as usePopupRootSync, b8 as useImplicitActiveTrigger, b9 as useOpenStateTransitions, h as createChangeEventDetails, k as imperativeAction, ba as PopupHandleAttachment, u as useBaseUiId, bb as useTriggerDataForwarding, f as useButton, V as useClick, bc as useOpenMethodTriggerProps, bd as CLICK_TRIGGER_IDENTIFIER, be as triggerOpenStateMapping, p as popupTransitionStateMapping, bf as CommonPopupDataAttributes, d as useStableCallback, o as ownerDocument, S as SafeReact, r as error, l as useOpenChangeComplete, F as FOCUSABLE_POPUP_PROPS, n as FloatingFocusManager, v as useControlled, z as none, bg as android, I as getWindow, y as addEventListener, bh as closeWatcher, a8 as useAnimationFrame, au as isElement, D as activeElement, bi as TransitionStatusDataAttributes, bj as swipe, bk as BASE_UI_SWIPE_IGNORE_SELECTOR, q as useTimeout, a7 as useDirection, a9 as webkit } from './useScrollLock-es.js';
4
4
  import { c as clamp, C as COMPOSITE_KEYS, u as useCSPContext, s as styleDisableScrollbar, n as normalizeScrollOffset } from './scrollEdges-es.js';
5
5
  import { jsx, jsxs } from 'react/jsx-runtime';
6
- import { K as getWindow, i as isHTMLElement, d as isLastTraversableNode, e as getParentNode, h as getComputedStyle$1, b as isElement } from './floating-ui.utils-es.js';
7
6
  import * as ReactDOM from 'react-dom';
7
+ import { f as findScrollableTouchTarget, h as hasScrollableAncestor } from './scrollable-es.js';
8
8
 
9
9
  function getEmptyRootContext() {
10
10
  return new FloatingRootStore({
@@ -58,12 +58,12 @@ function createPopupFloatingRootContext(triggerElements, floatingId, nested = fa
58
58
  onOpenChange: undefined
59
59
  });
60
60
  }
61
- const activeTriggerIdSelector = createSelector(state => state.triggerIdProp ?? state.activeTriggerId);
62
- const openSelector = createSelector(state => state.openProp ?? state.open);
63
- const popupIdSelector = createSelector(state => {
61
+ const activeTriggerIdSelector = state => state.triggerIdProp ?? state.activeTriggerId;
62
+ const openSelector = state => state.openProp ?? state.open;
63
+ const popupIdSelector = state => {
64
64
  const popupId = state.popupElement?.id ?? state.floatingId;
65
65
  return popupId || undefined;
66
- });
66
+ };
67
67
  function triggerOwnsOpenPopup(state, triggerId) {
68
68
  return triggerId !== undefined && openSelector(state) && activeTriggerIdSelector(state) === triggerId;
69
69
  }
@@ -75,47 +75,81 @@ function triggerOwnsOpenPopupOrIsOnlyTrigger(state, triggerId) {
75
75
  }
76
76
  const popupStoreSelectors = {
77
77
  open: openSelector,
78
- mounted: createSelector(state => state.mounted),
79
- transitionStatus: createSelector(state => state.transitionStatus),
80
- floatingRootContext: createSelector(state => state.floatingRootContext),
81
- triggerCount: createSelector(state => state.triggerCount),
82
- preventUnmountingOnClose: createSelector(state => state.preventUnmountingOnClose),
83
- payload: createSelector(state => state.payload),
78
+ mounted: state => state.mounted,
79
+ transitionStatus: state => state.transitionStatus,
80
+ floatingRootContext: state => state.floatingRootContext,
81
+ triggerCount: state => state.triggerCount,
82
+ preventUnmountingOnClose: state => state.preventUnmountingOnClose,
83
+ payload: state => state.payload,
84
84
  activeTriggerId: activeTriggerIdSelector,
85
- activeTriggerElement: createSelector(state => state.mounted ? state.activeTriggerElement : null),
85
+ activeTriggerElement: state => state.mounted ? state.activeTriggerElement : null,
86
86
  popupId: popupIdSelector,
87
87
  /**
88
88
  * Whether the trigger with the given ID was used to open the popup.
89
89
  */
90
- isTriggerActive: createSelector((state, triggerId) => triggerId !== undefined && activeTriggerIdSelector(state) === triggerId),
90
+ isTriggerActive: (state, triggerId) => triggerId !== undefined && activeTriggerIdSelector(state) === triggerId,
91
91
  /**
92
92
  * Whether the popup is open and was activated by a trigger with the given ID.
93
93
  */
94
- isOpenedByTrigger: createSelector((state, triggerId) => triggerOwnsOpenPopup(state, triggerId)),
94
+ isOpenedByTrigger: (state, triggerId) => triggerOwnsOpenPopup(state, triggerId),
95
95
  /**
96
96
  * Whether the popup is mounted and was activated by a trigger with the given ID.
97
97
  */
98
- isMountedByTrigger: createSelector((state, triggerId) => triggerId !== undefined && activeTriggerIdSelector(state) === triggerId && state.mounted),
99
- triggerProps: createSelector((state, isActive) => isActive ? state.activeTriggerProps : state.inactiveTriggerProps),
98
+ isMountedByTrigger: (state, triggerId) => triggerId !== undefined && activeTriggerIdSelector(state) === triggerId && state.mounted,
99
+ triggerProps: (state, isActive) => isActive ? state.activeTriggerProps : state.inactiveTriggerProps,
100
100
  /**
101
101
  * Popup id for the trigger that currently owns the open popup.
102
102
  */
103
- triggerPopupId: createSelector((state, triggerId) => triggerOwnsOpenPopupOrIsOnlyTrigger(state, triggerId) ? popupIdSelector(state) : undefined),
104
- popupProps: createSelector(state => state.popupProps),
105
- popupElement: createSelector(state => state.popupElement),
106
- positionerElement: createSelector(state => state.positionerElement)
103
+ triggerPopupId: (state, triggerId) => triggerOwnsOpenPopupOrIsOnlyTrigger(state, triggerId) ? popupIdSelector(state) : undefined,
104
+ popupProps: state => state.popupProps,
105
+ popupElement: state => state.popupElement,
106
+ positionerElement: state => state.positionerElement
107
107
  };
108
108
 
109
- const IsDrawerContext = /*#__PURE__*/React.createContext(false);
110
- if (process.env.NODE_ENV !== "production") IsDrawerContext.displayName = "IsDrawerContext";
109
+ /**
110
+ * Store members a detached handle-backed trigger reads or invokes for trigger registration and data
111
+ * forwarding. `set`/`update` are included only for trigger-count and trigger-data bookkeeping; on a
112
+ * detached (inert) store they are intentionally no-ops, so a write through them is not guaranteed to
113
+ * be durable. Component handle-store views Pick these from their concrete store (preserving its
114
+ * context and selectors) and add any component-specific trigger-invoked members such as `setOpen`.
115
+ */
116
+
117
+ /**
118
+ * The subset of a popup store that trigger registration and data forwarding rely on. Narrow enough
119
+ * that an inert store can be passed while detached.
120
+ */
121
+
122
+ /**
123
+ * Reads the store currently exposed by a popup handle and subscribes to store-pointer changes.
124
+ * Detached triggers use this to follow a handle as a root attaches or detaches: while no root is
125
+ * attached, the handle exposes its fallback store; once a root attaches, subscribers re-render and
126
+ * read from the live root store.
127
+ *
128
+ * Returns `undefined` when no handle is provided so callers can fall back to their root context.
129
+ *
130
+ * @param handle The popup handle to read from, or `undefined` when the trigger is not handle-bound.
131
+ */
132
+ function usePopupHandleStore(handle) {
133
+ const subscribe = React.useCallback(listener => {
134
+ if (handle === undefined) {
135
+ return NOOP;
136
+ }
137
+ return handle.subscribeStore(listener);
138
+ }, [handle]);
139
+ const getSnapshot = React.useCallback(() => {
140
+ return handle === undefined ? undefined : handle.store;
141
+ }, [handle]);
142
+ return shimExports.useSyncExternalStore(subscribe, getSnapshot, () => handle?.serverStore);
143
+ }
144
+
111
145
  const DialogRootContext = /*#__PURE__*/React.createContext(undefined);
112
146
  if (process.env.NODE_ENV !== "production") DialogRootContext.displayName = "DialogRootContext";
113
147
  function useDialogRootContext(optional) {
114
- const dialogRootContext = React.useContext(DialogRootContext);
115
- if (optional === false && dialogRootContext === undefined) {
148
+ const store = React.useContext(DialogRootContext);
149
+ if (!optional && store === undefined) {
116
150
  throw new Error(process.env.NODE_ENV !== "production" ? 'Base UI: DialogRootContext is missing. Dialog parts must be placed within <Dialog.Root>.' : formatErrorMessage(27));
117
151
  }
118
- return dialogRootContext;
152
+ return store;
119
153
  }
120
154
 
121
155
  const DialogPortalContext = /*#__PURE__*/React.createContext(undefined);
@@ -128,14 +162,26 @@ function useDialogPortalContext() {
128
162
  return value;
129
163
  }
130
164
 
165
+ /**
166
+ * Shared by `Dialog.Popup` and `Dialog.Viewport`, whose states have the same shape.
167
+ * `nested` is not mapped: unmapped `true` booleans already render as `data-nested`.
168
+ */
169
+ const dialogStateAttributesMapping = {
170
+ ...popupStateMapping,
171
+ ...transitionStatusMapping,
172
+ nestedDialogOpen(value) {
173
+ return value ? {
174
+ 'data-nested-dialog-open': ''
175
+ } : null;
176
+ }
177
+ };
178
+
131
179
  const DialogPortal = /*#__PURE__*/React.forwardRef(function DialogPortal(props, forwardedRef) {
132
180
  const {
133
181
  keepMounted = false,
134
182
  ...portalProps
135
183
  } = props;
136
- const {
137
- store
138
- } = useDialogRootContext();
184
+ const store = useDialogRootContext();
139
185
  const mounted = store.useState('mounted');
140
186
  const modal = store.useState('modal');
141
187
  const open = store.useState('open');
@@ -157,25 +203,6 @@ const DialogPortal = /*#__PURE__*/React.forwardRef(function DialogPortal(props,
157
203
  });
158
204
  if (process.env.NODE_ENV !== "production") DialogPortal.displayName = "DialogPortal";
159
205
 
160
- function useDialogRoot(params) {
161
- const {
162
- store,
163
- actionsRef
164
- } = params;
165
- const open = store.useState('open');
166
- usePopupRootSync(store, open);
167
- useImplicitActiveTrigger(store);
168
- const {
169
- forceUnmount
170
- } = useOpenStateTransitions(open, store);
171
- const handleImperativeClose = React.useCallback(() => {
172
- store.setOpen(false, createChangeEventDetails(imperativeAction));
173
- }, [store]);
174
- React.useImperativeHandle(actionsRef, () => ({
175
- unmount: forceUnmount,
176
- close: handleImperativeClose
177
- }), [forceUnmount, handleImperativeClose]);
178
- }
179
206
  function DialogInteractions({
180
207
  store,
181
208
  parentContext,
@@ -211,8 +238,18 @@ function DialogInteractions({
211
238
  if ('button' in event && event.button !== 0) {
212
239
  return false;
213
240
  }
214
- if ('touches' in event && event.touches.length !== 1) {
215
- return false;
241
+ if ('touches' in event) {
242
+ // Outside press can be handled on `touchend`, where the lifted point is
243
+ // reported in `changedTouches` and `touches` contains any remaining
244
+ // active points. Treat it as a single-finger tap only when exactly one
245
+ // touch ended and no other fingers are still down.
246
+ if (event.type === 'touchend') {
247
+ if (event.changedTouches.length !== 1 || event.touches.length !== 0) {
248
+ return false;
249
+ }
250
+ } else if (event.touches.length !== 1) {
251
+ return false;
252
+ }
216
253
  }
217
254
  const target = getTarget(event);
218
255
  if (isTopmost && !disablePointerDismissal) {
@@ -220,7 +257,9 @@ function DialogInteractions({
220
257
  // This supports multiple modal dialogs that aren't nested in the React tree:
221
258
  // https://github.com/mui/base-ui/issues/1320
222
259
  if (modal) {
223
- return store.context.internalBackdropRef.current || store.context.backdropRef.current ? store.context.internalBackdropRef.current === target || store.context.backdropRef.current === target || contains(target, popupElement) && !target?.hasAttribute('data-base-ui-portal') : true;
260
+ const internalBackdrop = store.context.internalBackdropRef.current;
261
+ const backdrop = store.context.backdropRef.current;
262
+ return internalBackdrop || backdrop ? internalBackdrop === target || backdrop === target || contains(target, popupElement) && !target?.hasAttribute('data-base-ui-portal') : true;
224
263
  }
225
264
  return true;
226
265
  }
@@ -231,73 +270,82 @@ function DialogInteractions({
231
270
  useScrollLock(open && modal === true, popupElement);
232
271
 
233
272
  // Listen for nested open/close events on this store to maintain the counts.
273
+ // A close notification is an open notification with zeroed counts.
234
274
  store.useContextCallback('onNestedDialogOpen', (dialogCount, drawerCount) => {
235
275
  setOwnNestedOpenDialogs(dialogCount);
236
276
  setOwnNestedOpenDrawers(drawerCount);
237
277
  });
238
- store.useContextCallback('onNestedDialogClose', () => {
239
- setOwnNestedOpenDialogs(0);
240
- setOwnNestedOpenDrawers(0);
241
- });
242
278
 
243
279
  // Notify parent of our open/close state using parent callbacks, if any
244
- React.useEffect(() => {
245
- if (parentContext?.onNestedDialogOpen && open) {
246
- parentContext.onNestedDialogOpen(ownNestedOpenDialogs + 1, ownNestedOpenDrawers + (isDrawer ? 1 : 0));
247
- }
248
- if (parentContext?.onNestedDialogClose && !open) {
249
- parentContext.onNestedDialogClose();
280
+ useIsoLayoutEffect(() => {
281
+ if (parentContext?.onNestedDialogOpen) {
282
+ if (open) {
283
+ parentContext.onNestedDialogOpen(ownNestedOpenDialogs + 1, ownNestedOpenDrawers + (isDrawer ? 1 : 0));
284
+ } else {
285
+ parentContext.onNestedDialogOpen(0, 0);
286
+ }
250
287
  }
251
288
  return () => {
252
- if (parentContext?.onNestedDialogClose && open) {
253
- parentContext.onNestedDialogClose();
289
+ if (parentContext?.onNestedDialogOpen && open) {
290
+ parentContext.onNestedDialogOpen(0, 0);
254
291
  }
255
292
  };
256
293
  }, [isDrawer, open, ownNestedOpenDialogs, ownNestedOpenDrawers, parentContext]);
257
- const activeTriggerProps = dismiss.reference ?? EMPTY_OBJECT;
258
- const inactiveTriggerProps = dismiss.trigger ?? EMPTY_OBJECT;
259
- // Consumers (DialogPopup/DrawerPopup) already spread `FOCUSABLE_POPUP_PROPS`
260
- // directly, so the popup props only need to carry the dismiss handlers.
261
- const popupProps = dismiss.floating ?? EMPTY_OBJECT;
262
294
  usePopupInteractionProps(store, {
263
- activeTriggerProps,
264
- inactiveTriggerProps,
265
- popupProps,
295
+ // `enabled` is not passed to `useDismiss`, so its props are always defined,
296
+ // and `trigger` is the same object as `reference`.
297
+ activeTriggerProps: dismiss.reference,
298
+ inactiveTriggerProps: dismiss.trigger,
299
+ // DialogPopup and DrawerPopup spread `FOCUSABLE_POPUP_PROPS` directly, so
300
+ // this only needs to carry the dismiss handlers.
301
+ popupProps: dismiss.floating,
266
302
  nestedOpenDialogCount: ownNestedOpenDialogs,
267
303
  nestedOpenDrawerCount: ownNestedOpenDrawers
268
304
  });
269
305
  return null;
270
306
  }
271
307
 
308
+ /**
309
+ * A `ReactStore` whose state never changes.
310
+ *
311
+ * Useful for fallback stores that need to support normal store reads while detached from the
312
+ * component that owns real state. Context values may still contain mutable refs or maps.
313
+ */
314
+ class NullStore extends ReactStore {
315
+ // `update`/`set`/`notifyAll` funnel through `setState` in the base `Store`, so overriding
316
+ // `setState` alone would neutralize them today. They are overridden explicitly so the store stays
317
+ // inert even if a future base-class change stops routing a mutator through `setState`.
318
+ setState(_newState) {}
319
+ update(_changes) {}
320
+ set(_key, _value) {}
321
+ notifyAll() {}
322
+ }
323
+
272
324
  const selectors = {
273
325
  ...popupStoreSelectors,
274
- modal: createSelector(state => state.modal),
275
- nested: createSelector(state => state.nested),
276
- nestedOpenDialogCount: createSelector(state => state.nestedOpenDialogCount),
277
- nestedOpenDrawerCount: createSelector(state => state.nestedOpenDrawerCount),
278
- disablePointerDismissal: createSelector(state => state.disablePointerDismissal),
279
- openMethod: createSelector(state => state.openMethod),
280
- descriptionElementId: createSelector(state => state.descriptionElementId),
281
- titleElementId: createSelector(state => state.titleElementId),
282
- viewportElement: createSelector(state => state.viewportElement),
283
- role: createSelector(state => state.role)
326
+ modal: state => state.modal,
327
+ nested: state => state.nested,
328
+ nestedOpenDialogCount: state => state.nestedOpenDialogCount,
329
+ nestedOpenDrawerCount: state => state.nestedOpenDrawerCount,
330
+ disablePointerDismissal: state => state.disablePointerDismissal,
331
+ openMethod: state => state.openMethod,
332
+ descriptionElementId: state => state.descriptionElementId,
333
+ titleElementId: state => state.titleElementId,
334
+ viewportElement: state => state.viewportElement,
335
+ role: state => state.role
284
336
  };
337
+
338
+ /**
339
+ * The subset of `DialogStore` that detached handle-backed triggers rely on. Both the real
340
+ * `DialogStore` and the inert fallback store satisfy it, so a trigger can read from whichever
341
+ * store the handle currently exposes.
342
+ */
343
+
285
344
  class DialogStore extends ReactStore {
286
- constructor(initialState, floatingId, nested = false) {
345
+ constructor(initialState, floatingId, nested) {
287
346
  const triggerElements = new PopupTriggerMap();
288
- const state = createInitialState(initialState);
289
- state.floatingRootContext = createPopupFloatingRootContext(triggerElements, floatingId, nested);
290
- super(state, {
291
- popupRef: /*#__PURE__*/React.createRef(),
292
- backdropRef: /*#__PURE__*/React.createRef(),
293
- internalBackdropRef: /*#__PURE__*/React.createRef(),
294
- outsidePressEnabledRef: {
295
- current: true
296
- },
297
- triggerElements,
298
- onOpenChange: undefined,
299
- onOpenChangeComplete: undefined
300
- }, selectors);
347
+ const state = createInitialState(initialState, triggerElements, floatingId, nested);
348
+ super(state, createInitialContext(triggerElements), selectors);
301
349
  }
302
350
  setOpen = (nextOpen, eventDetails) => {
303
351
  eventDetails.preventUnmountOnClose = () => {
@@ -319,20 +367,22 @@ class DialogStore extends ReactStore {
319
367
  setPopupOpenState(updatedState, nextOpen, eventDetails.trigger);
320
368
  this.update(updatedState);
321
369
  };
322
- static useStore(externalStore, initialState) {
323
- /* eslint-disable react-hooks/rules-of-hooks */
324
- const store = usePopupStore(externalStore, (floatingId, nested) => new DialogStore(initialState, floatingId, nested), true).store;
325
- /* eslint-enable react-hooks/rules-of-hooks */
370
+ }
326
371
 
327
- return store;
328
- }
372
+ /**
373
+ * Creates the inert fallback store used by detached handle-backed triggers while no
374
+ * `Dialog.Root` is attached. It preserves a dialog-specific trigger registry in context so
375
+ * detached triggers can register before migrating to the live root store.
376
+ */
377
+ function createNullDialogStore() {
378
+ const triggerElements = new PopupTriggerMap();
379
+ return new NullStore(Object.freeze(createInitialState(undefined, triggerElements)), Object.freeze(createInitialContext(triggerElements)), selectors);
329
380
  }
330
- function createInitialState(initialState = {}) {
331
- return {
381
+ function createInitialState(initialState, triggerElements, floatingId, nested = false) {
382
+ const state = {
332
383
  ...createInitialPopupStoreState(),
333
384
  modal: true,
334
385
  disablePointerDismissal: false,
335
- popupElement: null,
336
386
  viewportElement: null,
337
387
  descriptionElementId: undefined,
338
388
  titleElementId: undefined,
@@ -343,9 +393,24 @@ function createInitialState(initialState = {}) {
343
393
  role: 'dialog',
344
394
  ...initialState
345
395
  };
396
+ state.floatingRootContext = createPopupFloatingRootContext(triggerElements, floatingId, nested);
397
+ return state;
398
+ }
399
+ function createInitialContext(triggerElements) {
400
+ return {
401
+ popupRef: /*#__PURE__*/React.createRef(),
402
+ backdropRef: /*#__PURE__*/React.createRef(),
403
+ internalBackdropRef: /*#__PURE__*/React.createRef(),
404
+ outsidePressEnabledRef: {
405
+ current: true
406
+ },
407
+ triggerElements,
408
+ onOpenChange: undefined,
409
+ onOpenChangeComplete: undefined
410
+ };
346
411
  }
347
412
 
348
- function useRenderDialogRoot(props, mode = 'dialog') {
413
+ function useRenderDialogRoot(mode, props) {
349
414
  const {
350
415
  children,
351
416
  open: openProp,
@@ -364,38 +429,26 @@ function useRenderDialogRoot(props, mode = 'dialog') {
364
429
  const modal = isAlertDialog ? true : modalProp;
365
430
  const disablePointerDismissal = isAlertDialog || disablePointerDismissalProp;
366
431
  const role = isAlertDialog ? 'alertdialog' : 'dialog';
367
- const parentDialogRootContext = useDialogRootContext(true);
368
- const nested = Boolean(parentDialogRootContext);
432
+ const parentStore = useDialogRootContext(true);
433
+ const nested = parentStore != null;
369
434
  const rootState = {
370
435
  modal,
371
436
  disablePointerDismissal,
372
437
  nested,
373
438
  role
374
439
  };
375
- const store = DialogStore.useStore(handle?.store, {
440
+
441
+ // The store is owned by this Root instance and created exactly once. It is not tied to the handle:
442
+ // the handle attaches to it, so swapping the handle re-attaches rather than recreating state.
443
+ // Default values are only initial values; controlled values and root state are synced after creation.
444
+ // Dialogs pass the popup element to Floating UI as the floating element (`treatPopupAsFloatingElement`).
445
+ const store = usePopupRootStore((floatingId, floatingNested) => new DialogStore({
376
446
  open: defaultOpen,
377
447
  openProp,
378
448
  activeTriggerId: defaultTriggerIdProp,
379
449
  triggerIdProp,
380
450
  ...rootState
381
- });
382
-
383
- // Support initially open state when uncontrolled
384
- useOnFirstRender(() => {
385
- const nextState = openProp === undefined && store.state.open === false && defaultOpen === true ? {
386
- open: true,
387
- activeTriggerId: defaultTriggerIdProp
388
- } : null;
389
- if (isAlertDialog) {
390
- // Handles can reuse plain Dialog stores; alert dialog invariants must exist immediately.
391
- store.update(nextState ? {
392
- ...rootState,
393
- ...nextState
394
- } : rootState);
395
- } else if (nextState) {
396
- store.update(nextState);
397
- }
398
- });
451
+ }, floatingId, floatingNested), true);
399
452
  store.useControlledProp('openProp', openProp);
400
453
  store.useControlledProp('triggerIdProp', triggerIdProp);
401
454
  store.useSyncedValues(rootState);
@@ -404,83 +457,31 @@ function useRenderDialogRoot(props, mode = 'dialog') {
404
457
  const open = store.useState('open');
405
458
  const mounted = store.useState('mounted');
406
459
  const payload = store.useState('payload');
407
- useDialogRoot({
408
- store,
409
- actionsRef
410
- });
460
+ usePopupRootSync(store, open);
461
+ useImplicitActiveTrigger(store);
462
+ const {
463
+ forceUnmount
464
+ } = useOpenStateTransitions(open, store);
465
+ React.useImperativeHandle(actionsRef, () => ({
466
+ unmount: forceUnmount,
467
+ close: () => store.setOpen(false, createChangeEventDetails(imperativeAction))
468
+ }), [forceUnmount, store]);
411
469
  const shouldRenderInteractions = open || mounted;
412
- const contextValue = React.useMemo(() => ({
413
- store
414
- }), [store]);
415
- return /*#__PURE__*/jsx(IsDrawerContext.Provider, {
416
- value: false,
417
- children: /*#__PURE__*/jsxs(DialogRootContext.Provider, {
418
- value: contextValue,
419
- children: [shouldRenderInteractions && /*#__PURE__*/jsx(DialogInteractions, {
420
- store: store,
421
- parentContext: parentDialogRootContext?.store.context,
422
- isDrawer: isDrawer
423
- }), typeof children === 'function' ? children({
424
- payload
425
- }) : children]
426
- })
470
+ return /*#__PURE__*/jsxs(DialogRootContext.Provider, {
471
+ value: store,
472
+ children: [handle && /*#__PURE__*/jsx(PopupHandleAttachment, {
473
+ handle: handle,
474
+ store: store
475
+ }), shouldRenderInteractions && /*#__PURE__*/jsx(DialogInteractions, {
476
+ store: store,
477
+ parentContext: parentStore?.context,
478
+ isDrawer: isDrawer
479
+ }), typeof children === 'function' ? children({
480
+ payload
481
+ }) : children]
427
482
  });
428
483
  }
429
484
 
430
- /**
431
- * Groups all parts of the dialog.
432
- * Doesn't render its own HTML element.
433
- *
434
- * Documentation: [Base UI Dialog](https://base-ui.com/react/components/dialog)
435
- */
436
- function DialogRoot(props) {
437
- const mode = React.useContext(IsDrawerContext) ? 'drawer' : 'dialog';
438
- return useRenderDialogRoot(props, mode);
439
- }
440
-
441
- let DialogViewportDataAttributes = function (DialogViewportDataAttributes) {
442
- /**
443
- * Present when the dialog is open.
444
- */
445
- DialogViewportDataAttributes[DialogViewportDataAttributes["open"] = CommonPopupDataAttributes.open] = "open";
446
- /**
447
- * Present when the dialog is closed.
448
- */
449
- DialogViewportDataAttributes[DialogViewportDataAttributes["closed"] = CommonPopupDataAttributes.closed] = "closed";
450
- /**
451
- * Present when the dialog is animating in.
452
- */
453
- DialogViewportDataAttributes[DialogViewportDataAttributes["startingStyle"] = CommonPopupDataAttributes.startingStyle] = "startingStyle";
454
- /**
455
- * Present when the dialog is animating out.
456
- */
457
- DialogViewportDataAttributes[DialogViewportDataAttributes["endingStyle"] = CommonPopupDataAttributes.endingStyle] = "endingStyle";
458
- /**
459
- * Present when the dialog is nested within another dialog.
460
- */
461
- DialogViewportDataAttributes["nested"] = "data-nested";
462
- /**
463
- * Present when the dialog has other open dialogs nested within it.
464
- */
465
- DialogViewportDataAttributes["nestedDialogOpen"] = "data-nested-dialog-open";
466
- return DialogViewportDataAttributes;
467
- }({});
468
-
469
- const stateAttributesMapping$2 = {
470
- ...popupStateMapping,
471
- ...transitionStatusMapping,
472
- nested(value) {
473
- return value ? {
474
- [DialogViewportDataAttributes.nested]: ''
475
- } : null;
476
- },
477
- nestedDialogOpen(value) {
478
- return value ? {
479
- [DialogViewportDataAttributes.nestedDialogOpen]: ''
480
- } : null;
481
- }
482
- };
483
-
484
485
  /**
485
486
  * A positioning container for the dialog popup that can be made scrollable.
486
487
  * Renders a `<div>` element.
@@ -496,9 +497,7 @@ const DialogViewport = /*#__PURE__*/React.forwardRef(function DialogViewport(com
496
497
  ...elementProps
497
498
  } = componentProps;
498
499
  const keepMounted = useDialogPortalContext();
499
- const {
500
- store
501
- } = useDialogRootContext();
500
+ const store = useDialogRootContext();
502
501
  const open = store.useState('open');
503
502
  const nested = store.useState('nested');
504
503
  const transitionStatus = store.useState('transitionStatus');
@@ -517,7 +516,7 @@ const DialogViewport = /*#__PURE__*/React.forwardRef(function DialogViewport(com
517
516
  enabled: shouldRender,
518
517
  state,
519
518
  ref: [forwardedRef, setViewportElement],
520
- stateAttributesMapping: stateAttributesMapping$2,
519
+ stateAttributesMapping: dialogStateAttributesMapping,
521
520
  props: [{
522
521
  role: 'presentation',
523
522
  hidden: !mounted,
@@ -548,8 +547,9 @@ const DialogTrigger = /*#__PURE__*/React.forwardRef(function DialogTrigger(compo
548
547
  handle,
549
548
  ...elementProps
550
549
  } = componentProps;
551
- const dialogRootContext = useDialogRootContext(true);
552
- const store = handle?.store ?? dialogRootContext?.store;
550
+ const dialogRootStore = useDialogRootContext(true);
551
+ const handleStore = usePopupHandleStore(handle);
552
+ const store = handleStore ?? dialogRootStore;
553
553
  if (!store) {
554
554
  throw new Error(process.env.NODE_ENV !== "production" ? 'Base UI: <Dialog.Trigger> must be used within <Dialog.Root> or provided with a handle.' : formatErrorMessage(79));
555
555
  }
@@ -571,9 +571,7 @@ const DialogTrigger = /*#__PURE__*/React.forwardRef(function DialogTrigger(compo
571
571
  disabled,
572
572
  native: nativeButton
573
573
  });
574
- const click = useClick(floatingContext, {
575
- enabled: floatingContext != null
576
- });
574
+ const click = useClick(floatingContext);
577
575
  const interactionTypeProps = useOpenMethodTriggerProps(() => store.select('open'), interactionType => {
578
576
  store.set('openMethod', interactionType);
579
577
  });
@@ -645,11 +643,6 @@ let DrawerBackdropCssVars = /*#__PURE__*/function (DrawerBackdropCssVars) {
645
643
  return DrawerBackdropCssVars;
646
644
  }({});
647
645
 
648
- const stateAttributesMapping$1 = {
649
- ...popupStateMapping,
650
- ...transitionStatusMapping
651
- };
652
-
653
646
  /**
654
647
  * An overlay displayed beneath the popup.
655
648
  * Renders a `<div>` element.
@@ -664,9 +657,7 @@ const DrawerBackdrop = /*#__PURE__*/React.forwardRef(function DrawerBackdrop(com
664
657
  forceRender = false,
665
658
  ...elementProps
666
659
  } = componentProps;
667
- const {
668
- store
669
- } = useDialogRootContext();
660
+ const store = useDialogRootContext();
670
661
  const open = store.useState('open');
671
662
  const nested = store.useState('nested');
672
663
  const mounted = store.useState('mounted');
@@ -678,7 +669,7 @@ const DrawerBackdrop = /*#__PURE__*/React.forwardRef(function DrawerBackdrop(com
678
669
  return useRenderElement('div', componentProps, {
679
670
  state,
680
671
  ref: [store.context.backdropRef, forwardedRef],
681
- stateAttributesMapping: stateAttributesMapping$1,
672
+ stateAttributesMapping: popupTransitionStateMapping,
682
673
  props: [{
683
674
  role: 'presentation',
684
675
  hidden: !mounted,
@@ -722,9 +713,8 @@ if (process.env.NODE_ENV !== "production") DrawerContent.displayName = "DrawerCo
722
713
 
723
714
  const DrawerProviderContext = /*#__PURE__*/React.createContext(undefined);
724
715
  if (process.env.NODE_ENV !== "production") DrawerProviderContext.displayName = "DrawerProviderContext";
725
- function useDrawerProviderContext(optional) {
726
- const context = React.useContext(DrawerProviderContext);
727
- return context;
716
+ function useDrawerProviderContext() {
717
+ return React.useContext(DrawerProviderContext);
728
718
  }
729
719
 
730
720
  let DrawerPopupDataAttributes = function (DrawerPopupDataAttributes) {
@@ -737,7 +727,7 @@ let DrawerPopupDataAttributes = function (DrawerPopupDataAttributes) {
737
727
  */
738
728
  DrawerPopupDataAttributes[DrawerPopupDataAttributes["closed"] = CommonPopupDataAttributes.closed] = "closed";
739
729
  /**
740
- * Present when the drawer is animating in.
730
+ * Present when the drawer begins animating in.
741
731
  */
742
732
  DrawerPopupDataAttributes[DrawerPopupDataAttributes["startingStyle"] = CommonPopupDataAttributes.startingStyle] = "startingStyle";
743
733
  /**
@@ -776,7 +766,7 @@ const DrawerRootContext = /*#__PURE__*/React.createContext(undefined);
776
766
  if (process.env.NODE_ENV !== "production") DrawerRootContext.displayName = "DrawerRootContext";
777
767
  function useDrawerRootContext(optional) {
778
768
  const drawerRootContext = React.useContext(DrawerRootContext);
779
- if (optional === false && drawerRootContext === undefined) {
769
+ if (optional !== true && drawerRootContext === undefined) {
780
770
  throw new Error(process.env.NODE_ENV !== "production" ? 'Base UI: DrawerRootContext is missing. Drawer parts must be placed within <Drawer.Root>.' : formatErrorMessage(90));
781
771
  }
782
772
  return drawerRootContext;
@@ -817,22 +807,24 @@ function resolveSnapPointValue(snapPoint, viewportHeight, rootFontSize) {
817
807
  }
818
808
  return null;
819
809
  }
820
- function findClosestSnapPoint(height, points) {
821
- let closest = null;
810
+
811
+ /**
812
+ * Returns the index of the value closest to `target`, or `-1` if `values` is empty.
813
+ */
814
+ function closestSnapPointIndex(values, target) {
815
+ let closestIndex = -1;
822
816
  let closestDistance = Infinity;
823
- for (const point of points) {
824
- const distance = Math.abs(point.height - height);
817
+ for (let index = 0; index < values.length; index += 1) {
818
+ const distance = Math.abs(values[index] - target);
825
819
  if (distance < closestDistance) {
826
820
  closestDistance = distance;
827
- closest = point;
821
+ closestIndex = index;
828
822
  }
829
823
  }
830
- return closest;
824
+ return closestIndex;
831
825
  }
832
826
  function useDrawerSnapPoints() {
833
- const {
834
- store
835
- } = useDialogRootContext();
827
+ const store = useDialogRootContext();
836
828
  const {
837
829
  snapPoints,
838
830
  activeSnapPoint,
@@ -845,12 +837,7 @@ function useDrawerSnapPoints() {
845
837
  const measureViewportHeight = useStableCallback(() => {
846
838
  const doc = ownerDocument(viewportElement);
847
839
  const html = doc.documentElement;
848
- if (viewportElement) {
849
- setViewportHeight(viewportElement.offsetHeight);
850
- }
851
- if (!viewportElement) {
852
- setViewportHeight(html.clientHeight);
853
- }
840
+ setViewportHeight(viewportElement ? viewportElement.offsetHeight : html.clientHeight);
854
841
  const fontSize = parseFloat(getComputedStyle(html).fontSize);
855
842
  if (Number.isFinite(fontSize)) {
856
843
  setRootFontSize(fontSize);
@@ -872,12 +859,9 @@ function useDrawerSnapPoints() {
872
859
  return [];
873
860
  }
874
861
  const maxHeight = Math.min(popupHeight, viewportHeight);
875
- if (!Number.isFinite(maxHeight) || maxHeight <= 0) {
876
- return [];
877
- }
878
862
  const resolved = snapPoints.map(value => {
879
863
  const resolvedHeight = resolveSnapPointValue(value, viewportHeight, rootFontSize);
880
- if (resolvedHeight === null || !Number.isFinite(resolvedHeight)) {
864
+ if (resolvedHeight === null) {
881
865
  return null;
882
866
  }
883
867
  const clampedHeight = clamp(resolvedHeight, 0, maxHeight);
@@ -905,9 +889,6 @@ function useDrawerSnapPoints() {
905
889
  return deduped;
906
890
  }, [popupHeight, rootFontSize, snapPoints, viewportHeight]);
907
891
  const resolvedActiveSnapPoint = React.useMemo(() => {
908
- if (activeSnapPoint === undefined) {
909
- return resolvedSnapPoints[0];
910
- }
911
892
  if (activeSnapPoint === null) {
912
893
  return undefined;
913
894
  }
@@ -917,11 +898,11 @@ function useDrawerSnapPoints() {
917
898
  }
918
899
  const maxHeight = Math.min(popupHeight, viewportHeight);
919
900
  const resolvedHeight = resolveSnapPointValue(activeSnapPoint, viewportHeight, rootFontSize);
920
- if (resolvedHeight === null || !Number.isFinite(resolvedHeight)) {
901
+ if (resolvedHeight === null) {
921
902
  return undefined;
922
903
  }
923
904
  const clampedHeight = clamp(resolvedHeight, 0, maxHeight);
924
- return findClosestSnapPoint(clampedHeight, resolvedSnapPoints) ?? undefined;
905
+ return resolvedSnapPoints[closestSnapPointIndex(resolvedSnapPoints.map(point => point.height), clampedHeight)];
925
906
  }, [activeSnapPoint, popupHeight, resolvedSnapPoints, rootFontSize, viewportHeight]);
926
907
  return {
927
908
  snapPoints,
@@ -936,9 +917,8 @@ function useDrawerSnapPoints() {
936
917
 
937
918
  const DrawerViewportContext = /*#__PURE__*/React.createContext(null);
938
919
  if (process.env.NODE_ENV !== "production") DrawerViewportContext.displayName = "DrawerViewportContext";
939
- function useDrawerViewportContext(optional) {
940
- const context = React.useContext(DrawerViewportContext);
941
- return context;
920
+ function useDrawerViewportContext() {
921
+ return React.useContext(DrawerViewportContext);
942
922
  }
943
923
 
944
924
  let drawerSwipeVarsRegistered = false;
@@ -996,8 +976,7 @@ function removeCSSVariableInheritance$1() {
996
976
  drawerSwipeVarsRegistered = true;
997
977
  }
998
978
  const stateAttributesMapping = {
999
- ...popupStateMapping,
1000
- ...transitionStatusMapping,
979
+ ...popupTransitionStateMapping,
1001
980
  expanded(value) {
1002
981
  return value ? {
1003
982
  [DrawerPopupDataAttributes.expanded]: ''
@@ -1014,9 +993,9 @@ const stateAttributesMapping = {
1014
993
  } : null;
1015
994
  },
1016
995
  swipeDirection(value) {
1017
- return value ? {
996
+ return {
1018
997
  [DrawerPopupDataAttributes.swipeDirection]: value
1019
- } : null;
998
+ };
1020
999
  },
1021
1000
  swiping(value) {
1022
1001
  return value ? {
@@ -1040,9 +1019,7 @@ const DrawerPopup = /*#__PURE__*/React.forwardRef(function DrawerPopup(component
1040
1019
  initialFocus,
1041
1020
  ...elementProps
1042
1021
  } = componentProps;
1043
- const {
1044
- store
1045
- } = useDialogRootContext();
1022
+ const store = useDialogRootContext();
1046
1023
  const popupRef = store.context.popupRef;
1047
1024
  const {
1048
1025
  swipeDirection,
@@ -1081,6 +1058,8 @@ const DrawerPopup = /*#__PURE__*/React.forwardRef(function DrawerPopup(component
1081
1058
  const swipeStrength = swipe?.swipeStrength ?? null;
1082
1059
  const [popupHeight, setPopupHeight] = React.useState(0);
1083
1060
  const popupHeightRef = React.useRef(0);
1061
+
1062
+ /* istanbul ignore else -- process.env.NODE_ENV is a build-time constant. */
1084
1063
  if (process.env.NODE_ENV !== 'production') {
1085
1064
  // eslint-disable-next-line react-hooks/rules-of-hooks
1086
1065
  React.useEffect(() => {
@@ -1163,7 +1142,7 @@ const DrawerPopup = /*#__PURE__*/React.forwardRef(function DrawerPopup(component
1163
1142
  }
1164
1143
  };
1165
1144
  }, [nestedSwipeProgressStore, popupRef]);
1166
- React.useEffect(() => {
1145
+ useIsoLayoutEffect(() => {
1167
1146
  if (!open) {
1168
1147
  return undefined;
1169
1148
  }
@@ -1172,7 +1151,7 @@ const DrawerPopup = /*#__PURE__*/React.forwardRef(function DrawerPopup(component
1172
1151
  notifyParentFrontmostHeight?.(0);
1173
1152
  };
1174
1153
  }, [frontmostHeight, open, notifyParentFrontmostHeight]);
1175
- React.useEffect(() => {
1154
+ useIsoLayoutEffect(() => {
1176
1155
  if (!notifyParentHasNestedDrawer) {
1177
1156
  return undefined;
1178
1157
  }
@@ -1216,7 +1195,7 @@ const DrawerPopup = /*#__PURE__*/React.forwardRef(function DrawerPopup(component
1216
1195
  let dragStyles = swipe ? swipe.getDragStyles() : EMPTY_OBJECT;
1217
1196
  if (shouldApplySnapPoints && swipeDirection === 'down') {
1218
1197
  const baseOffset = activeSnapPointOffset ?? 0;
1219
- const movementValue = Number.parseFloat(String(dragStyles[DrawerPopupCssVars.swipeMovementY] ?? 0));
1198
+ const movementValue = Number.parseFloat(String(dragStyles[DrawerPopupCssVars.swipeMovementY]));
1220
1199
  if (swiping && Number.isFinite(movementValue)) {
1221
1200
  dragStyles = {
1222
1201
  ...dragStyles,
@@ -1326,6 +1305,7 @@ function DrawerRoot(props) {
1326
1305
  state: 'snapPoint'
1327
1306
  });
1328
1307
  const isNestedDrawerOpenRef = React.useRef(false);
1308
+ const swipeAreaActiveRef = React.useRef(false);
1329
1309
  const setActiveSnapPoint = useStableCallback((nextSnapPoint, eventDetails) => {
1330
1310
  const resolvedEventDetails = eventDetails ?? createChangeEventDetails(none);
1331
1311
  onSnapPointChange?.(nextSnapPoint, resolvedEventDetails);
@@ -1385,6 +1365,7 @@ function DrawerRoot(props) {
1385
1365
  });
1386
1366
  const contextValue = React.useMemo(() => ({
1387
1367
  swipeDirection,
1368
+ swipeAreaActiveRef,
1388
1369
  snapToSequentialPoints,
1389
1370
  snapPoints,
1390
1371
  activeSnapPoint: resolvedActiveSnapPoint,
@@ -1403,30 +1384,28 @@ function DrawerRoot(props) {
1403
1384
  notifyParentSwipingChange,
1404
1385
  notifyParentSwipeProgressChange,
1405
1386
  notifyParentHasNestedDrawer
1406
- }), [resolvedActiveSnapPoint, frontmostHeight, hasNestedDrawer, nestedSwiping, nestedSwipeProgressStore, notifyParentHasNestedDrawer, notifyParentSwipeProgressChange, notifyParentSwipingChange, notifyParentFrontmostHeight, onNestedDrawerPresenceChange, onNestedFrontmostHeightChange, onNestedSwipeProgressChange, onNestedSwipingChange, onPopupHeightChange, popupHeight, setActiveSnapPoint, snapPoints, snapToSequentialPoints, swipeDirection]);
1387
+ }), [resolvedActiveSnapPoint, frontmostHeight, hasNestedDrawer, nestedSwiping, nestedSwipeProgressStore, notifyParentHasNestedDrawer, notifyParentSwipeProgressChange, notifyParentSwipingChange, notifyParentFrontmostHeight, onNestedDrawerPresenceChange, onNestedFrontmostHeightChange, onNestedSwipeProgressChange, onNestedSwipingChange, onPopupHeightChange, popupHeight, setActiveSnapPoint, snapPoints, snapToSequentialPoints, swipeAreaActiveRef, swipeDirection]);
1407
1388
  const resolvedChildren = typeof children === 'function' ? payload => /*#__PURE__*/jsxs(React.Fragment, {
1408
1389
  children: [_DrawerProviderReport || (_DrawerProviderReport = /*#__PURE__*/jsx(DrawerProviderReporter, {})), children(payload)]
1409
1390
  }) : /*#__PURE__*/jsxs(React.Fragment, {
1410
1391
  children: [_DrawerProviderReport2 || (_DrawerProviderReport2 = /*#__PURE__*/jsx(DrawerProviderReporter, {})), children]
1411
1392
  });
1393
+ const dialog = useRenderDialogRoot('drawer', {
1394
+ open: openProp,
1395
+ defaultOpen,
1396
+ onOpenChange: handleOpenChange,
1397
+ onOpenChangeComplete,
1398
+ disablePointerDismissal,
1399
+ modal,
1400
+ actionsRef,
1401
+ handle,
1402
+ triggerId: triggerIdProp,
1403
+ defaultTriggerId: defaultTriggerIdProp,
1404
+ children: resolvedChildren
1405
+ });
1412
1406
  return /*#__PURE__*/jsx(DrawerRootContext.Provider, {
1413
1407
  value: contextValue,
1414
- children: /*#__PURE__*/jsx(IsDrawerContext.Provider, {
1415
- value: true,
1416
- children: /*#__PURE__*/jsx(DialogRoot, {
1417
- open: openProp,
1418
- defaultOpen: defaultOpen,
1419
- onOpenChange: handleOpenChange,
1420
- onOpenChangeComplete: onOpenChangeComplete,
1421
- disablePointerDismissal: disablePointerDismissal,
1422
- modal: modal,
1423
- actionsRef: actionsRef,
1424
- handle: handle,
1425
- triggerId: triggerIdProp,
1426
- defaultTriggerId: defaultTriggerIdProp,
1427
- children: resolvedChildren
1428
- })
1429
- })
1408
+ children: dialog
1430
1409
  });
1431
1410
  }
1432
1411
  function createNestedSwipeProgressStore() {
@@ -1453,29 +1432,25 @@ function createNestedSwipeProgressStore() {
1453
1432
  };
1454
1433
  }
1455
1434
  function DrawerProviderReporter() {
1456
- const drawerId = useId();
1457
1435
  const providerContext = useDrawerProviderContext();
1458
- const {
1459
- store
1460
- } = useDialogRootContext(false);
1436
+ const store = useDialogRootContext(false);
1437
+ const setDrawerOpen = providerContext?.setDrawerOpen;
1438
+ const removeDrawer = providerContext?.removeDrawer;
1461
1439
  const open = store.useState('open');
1462
1440
  const nestedOpenDialogCount = store.useState('nestedOpenDialogCount');
1463
1441
  const popupElement = store.useState('popupElement');
1464
1442
  const isTopmost = nestedOpenDialogCount === 0;
1465
- React.useEffect(() => {
1466
- if (!providerContext || drawerId == null) {
1443
+ useIsoLayoutEffect(() => {
1444
+ if (!removeDrawer) {
1467
1445
  return undefined;
1468
1446
  }
1469
1447
  return () => {
1470
- providerContext.removeDrawer(drawerId);
1448
+ removeDrawer(store);
1471
1449
  };
1472
- }, [drawerId, providerContext]);
1473
- React.useEffect(() => {
1474
- if (drawerId == null) {
1475
- return;
1476
- }
1477
- providerContext?.setDrawerOpen(drawerId, open);
1478
- }, [drawerId, open, providerContext]);
1450
+ }, [removeDrawer, store]);
1451
+ useIsoLayoutEffect(() => {
1452
+ setDrawerOpen?.(store, open);
1453
+ }, [open, setDrawerOpen, store]);
1479
1454
  React.useEffect(() => {
1480
1455
  // CloseWatcher enables the Android back gesture (Chromium-only).
1481
1456
  // Keep this Android-only for now to avoid interfering with Escape/nesting semantics on desktop due to `useDismiss`.
@@ -1503,53 +1478,11 @@ function DrawerProviderReporter() {
1503
1478
  return null;
1504
1479
  }
1505
1480
 
1506
- function isScrollable(element, axis,
1507
- // When true, a container that overflows only once extra space is added (e.g. drawer
1508
- // keyboard scroll slack) still counts, as long as it has layout size on the axis.
1509
- allowOverflowIntent = false) {
1510
- const style = getComputedStyle$1(element);
1511
- if (axis === 'vertical') {
1512
- const overflowY = style.overflowY;
1513
- if (overflowY !== 'auto' && overflowY !== 'scroll') {
1514
- return false;
1515
- }
1516
- return allowOverflowIntent ? element.clientHeight > 0 : element.scrollHeight > element.clientHeight;
1517
- }
1518
- const overflowX = style.overflowX;
1519
- if (overflowX !== 'auto' && overflowX !== 'scroll') {
1520
- return false;
1521
- }
1522
- return allowOverflowIntent ? element.clientWidth > 0 : element.scrollWidth > element.clientWidth;
1523
- }
1524
- function hasScrollableAncestor(target, root, axes) {
1525
- // `getParentNode` crosses shadow boundaries (and slots), so a target inside a shadow root
1526
- // still walks up to scrollable ancestors in the light DOM.
1527
- let node = target;
1528
- while (isHTMLElement(node) && node !== root && !isLastTraversableNode(node)) {
1529
- for (const axis of axes) {
1530
- if (isScrollable(node, axis)) {
1531
- return true;
1532
- }
1533
- }
1534
- node = getParentNode(node);
1535
- }
1536
- return false;
1537
- }
1538
- function findScrollableTouchTarget(target, root, axis = 'vertical', allowOverflowIntent = false) {
1539
- // `getParentNode` crosses shadow boundaries (and slots), so a target inside a shadow root
1540
- // still reaches a scrollable ancestor in the light DOM.
1541
- let node = isHTMLElement(target) ? target : null;
1542
- while (isHTMLElement(node) && node !== root && !isLastTraversableNode(node)) {
1543
- if (isScrollable(node, axis, allowOverflowIntent)) {
1544
- return node;
1545
- }
1546
- node = getParentNode(node);
1547
- }
1548
- return isScrollable(root, axis, allowOverflowIntent) ? root : null;
1549
- }
1550
-
1551
- function getElementAtPoint(doc, x, y) {
1552
- return typeof doc?.elementFromPoint === 'function' ? doc.elementFromPoint(x, y) : null;
1481
+ // `Document.elementFromPoint` retargets shadow content to the shadow host, which then fails
1482
+ // `contains()` checks against a popup inside that shadow root, so callers pass `getRootNode()`
1483
+ // (a document or a shadow root) rather than `ownerDocument()`.
1484
+ function getElementAtPoint(root, x, y) {
1485
+ return typeof root?.elementFromPoint === 'function' ? root.elementFromPoint(x, y) : null;
1553
1486
  }
1554
1487
 
1555
1488
  const DEFAULT_SWIPE_THRESHOLD = 40;
@@ -1696,6 +1629,7 @@ function useSwipeDismiss(options) {
1696
1629
  });
1697
1630
  const swipeProgressRef = React.useRef(0);
1698
1631
  const swipeThresholdRef = React.useRef(swipeThresholdDefault);
1632
+ const swipeThresholdFunctionRef = React.useRef(null);
1699
1633
  const swipeStartTimeRef = React.useRef(null);
1700
1634
  const lastDragSampleRef = React.useRef(null);
1701
1635
  const lastDragVelocityRef = React.useRef({
@@ -1717,15 +1651,12 @@ function useSwipeDismiss(options) {
1717
1651
  if (!direction) {
1718
1652
  return;
1719
1653
  }
1720
- if (typeof swipeThresholdProp !== 'function') {
1721
- swipeThresholdRef.current = swipeThresholdDefault;
1722
- return;
1723
- }
1724
1654
  const element = elementRef.current;
1725
- if (!element) {
1655
+ const thresholdFunction = swipeThresholdFunctionRef.current;
1656
+ if (!element || !thresholdFunction) {
1726
1657
  return;
1727
1658
  }
1728
- const value = swipeThresholdProp({
1659
+ const value = thresholdFunction({
1729
1660
  element,
1730
1661
  direction
1731
1662
  });
@@ -1803,6 +1734,7 @@ function useSwipeDismiss(options) {
1803
1734
  setDragDismissed(false);
1804
1735
  updateSwipeProgress(0);
1805
1736
  swipeThresholdRef.current = swipeThresholdDefault;
1737
+ swipeThresholdFunctionRef.current = null;
1806
1738
  dragStartPosRef.current = {
1807
1739
  x: 0,
1808
1740
  y: 0
@@ -1843,11 +1775,6 @@ function useSwipeDismiss(options) {
1843
1775
  lastProgressDetailsRef.current = null;
1844
1776
  syncDragStyles(false);
1845
1777
  }, [setSwiping, swipeThresholdDefault, syncDragStyles, updateSwipeProgress]);
1846
- React.useEffect(() => {
1847
- if (typeof swipeThresholdProp !== 'function') {
1848
- swipeThresholdRef.current = swipeThresholdDefault;
1849
- }
1850
- }, [swipeThresholdDefault, swipeThresholdProp]);
1851
1778
  function getPrimaryPointerPosition(event) {
1852
1779
  if ('touches' in event) {
1853
1780
  const touch = event.touches[0];
@@ -1868,8 +1795,8 @@ function useSwipeDismiss(options) {
1868
1795
  return event.pointerType === 'touch';
1869
1796
  }
1870
1797
  function getTargetAtPoint(position, nativeEvent) {
1871
- const doc = ownerDocument(elementRef.current);
1872
- const elementAtPoint = getElementAtPoint(doc, position.x, position.y);
1798
+ const root = elementRef.current?.getRootNode();
1799
+ const elementAtPoint = getElementAtPoint(root, position.x, position.y);
1873
1800
  const target = elementAtPoint ?? getTarget(nativeEvent);
1874
1801
  return target;
1875
1802
  }
@@ -1912,6 +1839,8 @@ function useSwipeDismiss(options) {
1912
1839
  swipeStartTimeRef.current = getValidTimeStamp(event.timeStamp);
1913
1840
  swipeCancelBaselineRef.current = position;
1914
1841
  lastMovePosRef.current = position;
1842
+ swipeThresholdRef.current = swipeThresholdDefault;
1843
+ swipeThresholdFunctionRef.current = typeof swipeThresholdProp === 'function' ? swipeThresholdProp : null;
1915
1844
  if (element) {
1916
1845
  elementSizeRef.current = {
1917
1846
  width: element.offsetWidth,
@@ -1976,12 +1905,9 @@ function useSwipeDismiss(options) {
1976
1905
  onCancel?.(event.nativeEvent);
1977
1906
  }
1978
1907
  function applyDirectionalDamping(deltaX, deltaY) {
1979
- const exponent = value => value >= 0 ? value ** 0.5 : -(Math.abs(value) ** 0.5);
1908
+ const exponent = value => Math.sign(value) * Math.abs(value) ** 0.5;
1980
1909
  const dampAxis = (delta, allowNegative, allowPositive) => {
1981
- if (!allowNegative && delta < 0) {
1982
- return exponent(delta);
1983
- }
1984
- if (!allowPositive && delta > 0) {
1910
+ if (!allowNegative && delta < 0 || !allowPositive && delta > 0) {
1985
1911
  return exponent(delta);
1986
1912
  }
1987
1913
  return delta;
@@ -1994,29 +1920,16 @@ function useSwipeDismiss(options) {
1994
1920
  };
1995
1921
  }
1996
1922
  function canSwipeFromScrollEdgeOnPendingMove(scrollTarget, deltaX, deltaY) {
1923
+ // Swiping toward the axis start edge (down/right) is allowed when scrolled to the start;
1924
+ // toward the end edge (up/left) when scrolled to the end.
1925
+ const canSwipeOnAxis = (delta, scrollOffset, maxScrollOffset, allowTowardStart, allowTowardEnd) => delta > 0 && scrollOffset <= 0 && allowTowardStart || delta < 0 && scrollOffset >= Math.max(0, maxScrollOffset) && allowTowardEnd;
1997
1926
  const absDeltaX = Math.abs(deltaX);
1998
1927
  const absDeltaY = Math.abs(deltaY);
1999
- const useVerticalAxis = hasVertical && deltaY !== 0 && (!hasHorizontal || absDeltaY >= absDeltaX);
2000
- if (useVerticalAxis) {
2001
- const maxScrollTop = Math.max(0, scrollTarget.scrollHeight - scrollTarget.clientHeight);
2002
- const atTop = scrollTarget.scrollTop <= 0;
2003
- const atBottom = scrollTarget.scrollTop >= maxScrollTop;
2004
- const movingDown = deltaY > 0;
2005
- const movingUp = deltaY < 0;
2006
- const canSwipeDown = movingDown && atTop && allowDown;
2007
- const canSwipeUp = movingUp && atBottom && allowUp;
2008
- return canSwipeDown || canSwipeUp;
2009
- }
2010
- const useHorizontalAxis = hasHorizontal && deltaX !== 0 && (!hasVertical || absDeltaX > absDeltaY);
2011
- if (useHorizontalAxis) {
2012
- const maxScrollLeft = Math.max(0, scrollTarget.scrollWidth - scrollTarget.clientWidth);
2013
- const atLeft = scrollTarget.scrollLeft <= 0;
2014
- const atRight = scrollTarget.scrollLeft >= maxScrollLeft;
2015
- const movingRight = deltaX > 0;
2016
- const movingLeft = deltaX < 0;
2017
- const canSwipeRight = movingRight && atLeft && allowRight;
2018
- const canSwipeLeft = movingLeft && atRight && allowLeft;
2019
- return canSwipeRight || canSwipeLeft;
1928
+ if (hasVertical && deltaY !== 0 && (!hasHorizontal || absDeltaY >= absDeltaX)) {
1929
+ return canSwipeOnAxis(deltaY, scrollTarget.scrollTop, scrollTarget.scrollHeight - scrollTarget.clientHeight, allowDown, allowUp);
1930
+ }
1931
+ if (hasHorizontal && deltaX !== 0 && (!hasVertical || absDeltaX > absDeltaY)) {
1932
+ return canSwipeOnAxis(deltaX, scrollTarget.scrollLeft, scrollTarget.scrollWidth - scrollTarget.clientWidth, allowRight, allowLeft);
2020
1933
  }
2021
1934
  return null;
2022
1935
  }
@@ -2037,7 +1950,7 @@ function useSwipeDismiss(options) {
2037
1950
  pendingSwipeRef.current = true;
2038
1951
  pendingSwipeStartPosRef.current = startPos;
2039
1952
  swipeFromScrollableRef.current = false;
2040
- sawPrimaryButtonsOnMoveRef.current = false;
1953
+ sawPrimaryButtonsOnMoveRef.current = !('touches' in event);
2041
1954
  const allowedToStart = canStart ? canStart(startPos, {
2042
1955
  nativeEvent: event.nativeEvent,
2043
1956
  direction: primaryDirection
@@ -2065,14 +1978,21 @@ function useSwipeDismiss(options) {
2065
1978
  event.preventDefault();
2066
1979
  }
2067
1980
  if (isFirstPointerMoveRef.current) {
2068
- // Adjust the starting position to the current position on the first move
2069
- // to account for the delay between pointerdown and the first pointermove on iOS.
2070
- dragStartPosRef.current = position;
2071
- const moveTime = getValidTimeStamp(event.timeStamp);
2072
- if (moveTime !== null) {
2073
- swipeStartTimeRef.current = moveTime;
2074
- }
2075
1981
  isFirstPointerMoveRef.current = false;
1982
+ // Reset the drag origin to the first move's position to absorb the gap between the press and
1983
+ // the first move event — notably on iOS touch, where the first `touchmove` arrives already
1984
+ // offset from the `touchstart` — which would otherwise make the dragged element jump. This
1985
+ // only matters when an element follows the pointer; when `trackDrag` is false (e.g. the
1986
+ // swipe-area, which only opens the drawer) keep the original press position so a quick flick
1987
+ // still registers: on a low-refresh-rate display the whole travel can land in this single
1988
+ // first move, and discarding it would drop the gesture.
1989
+ if (trackDrag) {
1990
+ dragStartPosRef.current = position;
1991
+ const moveTime = getValidTimeStamp(event.timeStamp);
1992
+ if (moveTime !== null) {
1993
+ swipeStartTimeRef.current = moveTime;
1994
+ }
1995
+ }
2076
1996
  }
2077
1997
  const clientX = position.x;
2078
1998
  const clientY = position.y;
@@ -2160,51 +2080,45 @@ function useSwipeDismiss(options) {
2160
2080
  newOffsetY += dampedDelta.y;
2161
2081
  }
2162
2082
  }
2083
+
2084
+ // Only rewrite drag styles when the drag offset actually changed. `syncDragStyles` writes the
2085
+ // raw (undamped) frozen transform and movement vars, relying on the consumer's `onProgress`
2086
+ // to overwrite them with damped styles — but `updateSwipeProgress` dedupes unchanged
2087
+ // deltas and skips `onProgress`. A move that doesn't change the offset (e.g. the cursor
2088
+ // pinned at a screen edge during an off-screen drag, jittering only on the ignored axis)
2089
+ // would otherwise reinstate the raw styles with no correction, jumping the element to the
2090
+ // undamped position.
2091
+ const previousOffset = dragOffsetRef.current;
2092
+ const offsetChanged = newOffsetX !== previousOffset.x || newOffsetY !== previousOffset.y;
2163
2093
  dragOffsetRef.current = {
2164
2094
  x: newOffsetX,
2165
2095
  y: newOffsetY
2166
2096
  };
2167
- syncDragStyles(true);
2097
+ if (offsetChanged) {
2098
+ syncDragStyles(true);
2099
+ }
2168
2100
  recordDragSample({
2169
2101
  x: newOffsetX,
2170
2102
  y: newOffsetY
2171
2103
  }, getValidTimeStamp(event.timeStamp));
2172
2104
  const dragDeltaX = newOffsetX - initialTransformRef.current.x;
2173
2105
  const dragDeltaY = newOffsetY - initialTransformRef.current.y;
2174
- const swipeDirectionDetails = intendedSwipeDirectionRef.current;
2175
- const progressDirection = primaryDirection ?? intendedSwipeDirectionRef.current;
2176
- if (!progressDirection) {
2177
- updateSwipeProgress(0, {
2178
- deltaX: dragDeltaX,
2179
- deltaY: dragDeltaY,
2180
- direction: swipeDirectionDetails
2181
- });
2182
- return;
2183
- }
2184
- const size = progressDirection === 'left' || progressDirection === 'right' ? elementSizeRef.current.width : elementSizeRef.current.height;
2185
- const scale = initialTransformRef.current.scale || 1;
2186
- if (size <= 0 || scale <= 0) {
2187
- updateSwipeProgress(0, {
2188
- deltaX: dragDeltaX,
2189
- deltaY: dragDeltaY,
2190
- direction: swipeDirectionDetails
2191
- });
2192
- return;
2193
- }
2194
- const progressDisplacement = getDisplacement(progressDirection, newOffsetX - initialTransformRef.current.x, newOffsetY - initialTransformRef.current.y);
2195
- if (progressDisplacement <= 0) {
2196
- updateSwipeProgress(0, {
2197
- deltaX: dragDeltaX,
2198
- deltaY: dragDeltaY,
2199
- direction: swipeDirectionDetails
2200
- });
2201
- return;
2202
- }
2203
- updateSwipeProgress(progressDisplacement / (size * scale), {
2106
+ const progressDetails = {
2204
2107
  deltaX: dragDeltaX,
2205
2108
  deltaY: dragDeltaY,
2206
- direction: swipeDirectionDetails
2207
- });
2109
+ direction: intendedSwipeDirectionRef.current
2110
+ };
2111
+ let progress = 0;
2112
+ const progressDirection = primaryDirection ?? intendedSwipeDirectionRef.current;
2113
+ if (progressDirection) {
2114
+ const size = progressDirection === 'left' || progressDirection === 'right' ? elementSizeRef.current.width : elementSizeRef.current.height;
2115
+ const scale = initialTransformRef.current.scale || 1;
2116
+ const progressDisplacement = getDisplacement(progressDirection, dragDeltaX, dragDeltaY);
2117
+ if (size > 0 && scale > 0 && progressDisplacement > 0) {
2118
+ progress = progressDisplacement / (size * scale);
2119
+ }
2120
+ }
2121
+ updateSwipeProgress(progress, progressDetails);
2208
2122
  }
2209
2123
  const handleEnd = useStableCallback(event => {
2210
2124
  if (!enabled) {
@@ -2292,33 +2206,9 @@ function useSwipeDismiss(options) {
2292
2206
  dismissDirection = intendedSwipeDirectionRef.current ?? primaryDirection;
2293
2207
  } else {
2294
2208
  for (const direction of directions) {
2295
- switch (direction) {
2296
- case 'right':
2297
- if (deltaX > swipeThresholdRef.current) {
2298
- shouldClose = true;
2299
- dismissDirection = 'right';
2300
- }
2301
- break;
2302
- case 'left':
2303
- if (deltaX < -swipeThresholdRef.current) {
2304
- shouldClose = true;
2305
- dismissDirection = 'left';
2306
- }
2307
- break;
2308
- case 'down':
2309
- if (deltaY > swipeThresholdRef.current) {
2310
- shouldClose = true;
2311
- dismissDirection = 'down';
2312
- }
2313
- break;
2314
- case 'up':
2315
- if (deltaY < -swipeThresholdRef.current) {
2316
- shouldClose = true;
2317
- dismissDirection = 'up';
2318
- }
2319
- break;
2320
- }
2321
- if (shouldClose) {
2209
+ if (getDisplacement(direction, deltaX, deltaY) > swipeThresholdRef.current) {
2210
+ shouldClose = true;
2211
+ dismissDirection = direction;
2322
2212
  break;
2323
2213
  }
2324
2214
  }
@@ -2460,25 +2350,29 @@ function useSwipeDismiss(options) {
2460
2350
  });
2461
2351
  });
2462
2352
  const getDragStyles = React.useCallback(() => {
2353
+ // Read `isSwipingRef`, not the lagging `isSwiping` state, to match the imperative writer
2354
+ // `syncDragStyles`. Otherwise a render that commits before `setSwiping(true)` flushes strips the
2355
+ // transform it just wrote, flashing the popup to its resting position for a frame.
2356
+ const swiping = isSwipingRef.current;
2463
2357
  const dragOffset = dragOffsetRef.current;
2464
2358
  const initialTransform = initialTransformRef.current;
2465
2359
  const deltaX = dragOffset.x - initialTransform.x;
2466
2360
  const deltaY = dragOffset.y - initialTransform.y;
2467
- if (!isSwiping && deltaX === 0 && deltaY === 0 && !dragDismissed) {
2361
+ if (!swiping && deltaX === 0 && deltaY === 0 && !dragDismissed) {
2468
2362
  return {
2469
2363
  [movementCssVars.x]: '0px',
2470
2364
  [movementCssVars.y]: '0px'
2471
2365
  };
2472
2366
  }
2473
2367
  return {
2474
- transition: isSwiping ? 'none' : undefined,
2368
+ transition: swiping ? 'none' : undefined,
2475
2369
  // While swiping, freeze the element at its current visual transform so it doesn't snap to the
2476
2370
  // end position.
2477
- transform: isSwiping ? getDragTransform(dragOffset, initialTransform.scale) : undefined,
2371
+ transform: swiping ? getDragTransform(dragOffset, initialTransform.scale) : undefined,
2478
2372
  [movementCssVars.x]: `${deltaX}px`,
2479
2373
  [movementCssVars.y]: `${deltaY}px`
2480
2374
  };
2481
- }, [dragDismissed, isSwiping, movementCssVars]);
2375
+ }, [dragDismissed, movementCssVars]);
2482
2376
  const getPointerProps = React.useCallback(() => {
2483
2377
  if (!enabled) {
2484
2378
  return {};
@@ -2538,6 +2432,8 @@ const MIN_SWIPE_RELEASE_DURATION_MS = 80;
2538
2432
  const MAX_SWIPE_RELEASE_DURATION_MS = 360;
2539
2433
  const MIN_SWIPE_RELEASE_SCALAR = 0.1;
2540
2434
  const MAX_SWIPE_RELEASE_SCALAR = 1;
2435
+ const AXIS_LOCK_SLOP = 6;
2436
+ const AXIS_LOCK_BIAS = 2;
2541
2437
  const DRAWER_CONTENT_SELECTOR = `[${DRAWER_CONTENT_ATTRIBUTE}]`;
2542
2438
  /**
2543
2439
  * A positioning container for the drawer popup that can be made scrollable.
@@ -2553,9 +2449,7 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
2553
2449
  children,
2554
2450
  ...elementProps
2555
2451
  } = props;
2556
- const {
2557
- store
2558
- } = useDialogRootContext();
2452
+ const store = useDialogRootContext();
2559
2453
  const popupRef = store.context.popupRef;
2560
2454
  const backdropRef = store.context.backdropRef;
2561
2455
  const {
@@ -2563,7 +2457,8 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
2563
2457
  notifyParentSwipingChange,
2564
2458
  notifyParentSwipeProgressChange,
2565
2459
  frontmostHeight,
2566
- snapToSequentialPoints
2460
+ snapToSequentialPoints,
2461
+ swipeAreaActiveRef
2567
2462
  } = useDrawerRootContext();
2568
2463
  const providerContext = useDrawerProviderContext();
2569
2464
  const {
@@ -2597,30 +2492,13 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
2597
2492
  const touchScrollStateRef = React.useRef(null);
2598
2493
  const virtualKeyboard = useDrawerVirtualKeyboardContext();
2599
2494
  const snapPointRange = React.useMemo(() => {
2600
- if (!snapPoints || snapPoints.length < 2) {
2601
- return null;
2602
- }
2603
- if (swipeDirection !== 'down' && swipeDirection !== 'up') {
2604
- return null;
2605
- }
2606
- if (resolvedSnapPoints.length < 2) {
2607
- return null;
2608
- }
2609
- const offsets = resolvedSnapPoints.map(point => point.offset).filter(offset => Number.isFinite(offset)).sort((a, b) => a - b);
2610
- if (offsets.length < 2) {
2495
+ if (!snapPoints || snapPoints.length < 2 || resolvedSnapPoints.length < 2 || swipeDirection !== 'down' && swipeDirection !== 'up') {
2611
2496
  return null;
2612
2497
  }
2498
+ const offsets = resolvedSnapPoints.map(point => point.offset).sort((a, b) => a - b);
2613
2499
  const minOffset = offsets[0];
2614
2500
  const nextOffset = offsets[1];
2615
- const maxOffset = offsets[offsets.length - 1];
2616
- let range = nextOffset - minOffset;
2617
- if (!Number.isFinite(range) || range <= 0) {
2618
- const fallbackRange = maxOffset - minOffset;
2619
- if (!Number.isFinite(fallbackRange) || fallbackRange <= 0) {
2620
- return null;
2621
- }
2622
- range = fallbackRange;
2623
- }
2501
+ const range = nextOffset - minOffset;
2624
2502
  return {
2625
2503
  minOffset,
2626
2504
  range
@@ -2639,7 +2517,8 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
2639
2517
  return [swipeDirection];
2640
2518
  }, [snapPoints, swipeDirection]);
2641
2519
  const setSwipeDismissed = useStableCallback(dismissed => {
2642
- setSwipeDismissedElements(popupRef.current, backdropRef.current, dismissed);
2520
+ popupRef.current?.toggleAttribute(DrawerPopupDataAttributes.swipeDismiss, dismissed);
2521
+ backdropRef.current?.toggleAttribute(DrawerPopupDataAttributes.swipeDismiss, dismissed);
2643
2522
  });
2644
2523
  const clearSwipeRelease = useStableCallback(() => {
2645
2524
  setSwipeDismissed(false);
@@ -2653,11 +2532,7 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
2653
2532
  nestedSwipeActiveRef.current = false;
2654
2533
  notifyParentSwipingChange?.(false);
2655
2534
  });
2656
- const applySwipeProgress = useStableCallback(({
2657
- resolvedProgress,
2658
- shouldTrackProgress,
2659
- notifyParent
2660
- }) => {
2535
+ const applySwipeProgress = useStableCallback((resolvedProgress, shouldTrackProgress, notifyParent) => {
2661
2536
  const isActive = open && !nested && shouldTrackProgress;
2662
2537
  const swipeProgress = isActive ? resolvedProgress : 0;
2663
2538
  const nestedSwipeProgress = open && shouldTrackProgress ? resolvedProgress : 0;
@@ -2675,78 +2550,48 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
2675
2550
  if (!backdropElement) {
2676
2551
  return;
2677
2552
  }
2678
- if (!isActive || swipeProgress <= 0) {
2679
- backdropElement.style.setProperty(DrawerBackdropCssVars.swipeProgress, '0');
2680
- backdropElement.style.removeProperty(DrawerPopupCssVars.height);
2681
- return;
2682
- }
2683
- backdropElement.style.setProperty(DrawerBackdropCssVars.swipeProgress, `${swipeProgress}`);
2684
- if (frontmostHeight > 0) {
2553
+ const showProgress = isActive && swipeProgress > 0;
2554
+ backdropElement.style.setProperty(DrawerBackdropCssVars.swipeProgress, showProgress ? `${swipeProgress}` : '0');
2555
+ if (showProgress && frontmostHeight > 0) {
2685
2556
  backdropElement.style.setProperty(DrawerPopupCssVars.height, `${frontmostHeight}px`);
2686
2557
  } else {
2687
2558
  backdropElement.style.removeProperty(DrawerPopupCssVars.height);
2688
2559
  }
2689
2560
  });
2690
- function resolveSwipeRelease({
2691
- direction,
2692
- deltaX,
2693
- deltaY,
2694
- velocityX,
2695
- velocityY,
2696
- releaseVelocityX,
2697
- releaseVelocityY
2698
- }) {
2699
- if (!direction) {
2700
- return null;
2701
- }
2702
- const popupElement = store.context.popupRef.current;
2703
- if (!popupElement) {
2704
- return null;
2705
- }
2706
- const size = direction === 'left' || direction === 'right' ? popupElement.offsetWidth : popupElement.offsetHeight;
2707
- if (!Number.isFinite(size) || size <= 0) {
2561
+ function resolveSwipeRelease(popupElement, direction, deltaX, deltaY, velocityX, velocityY, releaseVelocityX, releaseVelocityY) {
2562
+ const size = getBaseSwipeSize(popupElement, direction);
2563
+ if (size <= 0) {
2708
2564
  return null;
2709
2565
  }
2710
- const axisDelta = direction === 'left' || direction === 'right' ? deltaX : deltaY;
2711
- const snapPointBaseOffset = snapPoints && snapPoints.length > 0 ? activeSnapPointOffset ?? 0 : 0;
2712
- let baseOffset = 0;
2713
- if (direction === 'down') {
2714
- baseOffset = snapPointBaseOffset;
2715
- } else if (direction === 'up') {
2716
- baseOffset = -snapPointBaseOffset;
2717
- }
2718
- const translation = baseOffset + axisDelta;
2719
- const translationAlongDirection = direction === 'left' || direction === 'up' ? -translation : translation;
2566
+
2567
+ // The snap point base offset shifts the popup along the dismiss direction for both
2568
+ // `down` (+offset) and `up` (-offset), so it always adds to the directional translation.
2569
+ const snapPointBaseOffset = (direction === 'down' || direction === 'up') && snapPoints && snapPoints.length > 0 ? activeSnapPointOffset ?? 0 : 0;
2570
+ const translationAlongDirection = snapPointBaseOffset + getDisplacement(direction, deltaX, deltaY);
2720
2571
  const remainingDistance = Math.max(0, size - translationAlongDirection);
2721
- if (!Number.isFinite(remainingDistance) || remainingDistance <= 0) {
2572
+ if (remainingDistance <= 0) {
2722
2573
  return null;
2723
2574
  }
2724
- const axisVelocity = direction === 'left' || direction === 'right' ? releaseVelocityX : releaseVelocityY;
2725
- const fallbackVelocity = direction === 'left' || direction === 'right' ? velocityX : velocityY;
2726
- const resolvedVelocity = Math.abs(axisVelocity) > 0 && Number.isFinite(axisVelocity) ? axisVelocity : fallbackVelocity;
2727
- const directionalVelocity = direction === 'left' || direction === 'up' ? -resolvedVelocity : resolvedVelocity;
2728
- if (!Number.isFinite(directionalVelocity) || directionalVelocity <= MIN_SWIPE_RELEASE_VELOCITY) {
2575
+ const releaseVelocity = getDisplacement(direction, releaseVelocityX, releaseVelocityY);
2576
+ const directionalVelocity = Math.abs(releaseVelocity) > 0 ? releaseVelocity : getDisplacement(direction, velocityX, velocityY);
2577
+ if (directionalVelocity <= MIN_SWIPE_RELEASE_VELOCITY) {
2729
2578
  return null;
2730
2579
  }
2731
2580
  const clampedVelocity = clamp(directionalVelocity, MIN_SWIPE_RELEASE_VELOCITY, MAX_SWIPE_RELEASE_VELOCITY);
2581
+ // The gesture hook supplies finite deltas and velocities. The guards above keep the remaining
2582
+ // distance and divisor positive, so the duration stays within [MIN, MAX] and the resulting
2583
+ // scalar within (0, 1].
2732
2584
  const durationMs = clamp(remainingDistance / clampedVelocity, MIN_SWIPE_RELEASE_DURATION_MS, MAX_SWIPE_RELEASE_DURATION_MS);
2733
- if (!Number.isFinite(durationMs)) {
2734
- return null;
2735
- }
2736
2585
  const normalizedDuration = (durationMs - MIN_SWIPE_RELEASE_DURATION_MS) / (MAX_SWIPE_RELEASE_DURATION_MS - MIN_SWIPE_RELEASE_DURATION_MS);
2737
- const durationScalar = clamp(MIN_SWIPE_RELEASE_SCALAR + normalizedDuration * (MAX_SWIPE_RELEASE_SCALAR - MIN_SWIPE_RELEASE_SCALAR), MIN_SWIPE_RELEASE_SCALAR, MAX_SWIPE_RELEASE_SCALAR);
2738
- if (!Number.isFinite(durationScalar) || durationScalar <= 0) {
2739
- return null;
2740
- }
2741
- return durationScalar;
2586
+ return MIN_SWIPE_RELEASE_SCALAR + normalizedDuration * (MAX_SWIPE_RELEASE_SCALAR - MIN_SWIPE_RELEASE_SCALAR);
2742
2587
  }
2743
2588
  function updateNestedSwipeActive(details) {
2744
2589
  if (nestedSwipeActiveRef.current || !details) {
2745
2590
  return;
2746
2591
  }
2747
2592
  const direction = details.direction ?? swipeDirection;
2748
- const delta = direction === 'left' || direction === 'right' ? details.deltaX : details.deltaY;
2749
- if (!Number.isFinite(delta) || Math.abs(delta) < MIN_SWIPE_THRESHOLD) {
2593
+ const delta = getDisplacement(direction, details.deltaX, details.deltaY);
2594
+ if (Math.abs(delta) < MIN_SWIPE_THRESHOLD) {
2750
2595
  return;
2751
2596
  }
2752
2597
  nestedSwipeActiveRef.current = true;
@@ -2763,13 +2608,10 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
2763
2608
  y: DrawerPopupCssVars.swipeMovementY
2764
2609
  },
2765
2610
  onSwipeStart(event) {
2766
- if ('touches' in event || 'pointerType' in event && event.pointerType === 'touch') {
2611
+ if ('touches' in event || event.pointerType === 'touch') {
2767
2612
  return;
2768
2613
  }
2769
2614
  const popupElement = popupRef.current;
2770
- if (!popupElement) {
2771
- return;
2772
- }
2773
2615
  const doc = ownerDocument(popupElement);
2774
2616
  const selection = doc.getSelection?.();
2775
2617
  if (!selection || selection.isCollapsed) {
@@ -2801,54 +2643,38 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
2801
2643
  return false;
2802
2644
  }
2803
2645
  const doc = popupElement.ownerDocument;
2804
- const elementAtPoint = getElementAtPoint(doc, position.x, position.y);
2646
+ const elementAtPoint = getElementAtPoint(popupElement.getRootNode(), position.x, position.y);
2805
2647
  if (!elementAtPoint || !contains(popupElement, elementAtPoint)) {
2806
2648
  return false;
2807
2649
  }
2808
2650
  const nativeEvent = details.nativeEvent;
2809
- const touchLike = 'touches' in nativeEvent || 'pointerType' in nativeEvent && nativeEvent.pointerType === 'touch';
2651
+ const touchLike = 'touches' in nativeEvent || nativeEvent.pointerType === 'touch';
2810
2652
  if (touchLike && shouldIgnoreSwipeForTextSelection(doc, popupElement)) {
2811
2653
  return false;
2812
2654
  }
2813
- if (nativeEvent.type === 'touchstart' && isSwipeIgnoredTarget(elementAtPoint)) {
2814
- return false;
2815
- }
2816
2655
  return true;
2817
2656
  },
2818
2657
  onProgress(progress, details) {
2819
2658
  updateNestedSwipeActive(details);
2820
2659
  const hasSnapPoints = Boolean(snapPoints && snapPoints.length > 0);
2821
- if (swipingRef.current && swipeDirection === 'down' && hasSnapPoints && details && Number.isFinite(details.deltaY)) {
2660
+ if (swipingRef.current && swipeDirection === 'down' && hasSnapPoints && details) {
2822
2661
  const popupElement = store.context.popupRef.current;
2823
2662
  if (popupElement) {
2824
2663
  popupElement.style.removeProperty('transform');
2825
2664
  popupElement.style.setProperty(DrawerPopupCssVars.swipeMovementY, `${getSnapPointSwipeMovement(activeSnapPointOffset ?? 0, details.deltaY)}px`);
2826
2665
  }
2827
2666
  }
2828
- const currentDirection = details?.direction ?? swipe$1.swipeDirection;
2829
- const isDismissSwipe = currentDirection === undefined || currentDirection === swipeDirection;
2830
- const isVerticalSwipe = swipeDirection === 'down' || swipeDirection === 'up';
2831
- const shouldTrackProgress = hasSnapPoints && isVerticalSwipe || !hasSnapPoints || swipeDirection === 'left' || swipeDirection === 'right' || isDismissSwipe;
2832
2667
  let resolvedProgress = progress;
2833
2668
  if (snapPointRange && popupHeight > 0) {
2669
+ const baseOffset = activeSnapPointOffset ?? snapPointRange.minOffset;
2670
+ const offsetToProgress = nextOffset => clamp((nextOffset - snapPointRange.minOffset) / snapPointRange.range, 0, 1);
2834
2671
  if (details && Number.isFinite(details.deltaY)) {
2835
- const baseOffset = activeSnapPointOffset ?? snapPointRange.minOffset;
2836
- const nextOffset = clamp(baseOffset + details.deltaY, 0, popupHeight);
2837
- resolvedProgress = clamp((nextOffset - snapPointRange.minOffset) / snapPointRange.range, 0, 1);
2672
+ resolvedProgress = offsetToProgress(clamp(baseOffset + details.deltaY, 0, popupHeight));
2838
2673
  } else if (snapPointProgress !== null) {
2839
2674
  resolvedProgress = snapPointProgress;
2840
- } else if (currentDirection === 'down' || currentDirection === 'up') {
2841
- const displacement = progress * popupHeight;
2842
- const baseOffset = activeSnapPointOffset ?? snapPointRange.minOffset;
2843
- const nextOffset = currentDirection === 'down' ? baseOffset + displacement : baseOffset - displacement;
2844
- resolvedProgress = clamp((nextOffset - snapPointRange.minOffset) / snapPointRange.range, 0, 1);
2845
2675
  }
2846
2676
  }
2847
- applySwipeProgress({
2848
- resolvedProgress,
2849
- shouldTrackProgress,
2850
- notifyParent: true
2851
- });
2677
+ applySwipeProgress(resolvedProgress, true, true);
2852
2678
  },
2853
2679
  onRelease({
2854
2680
  event,
@@ -2860,30 +2686,21 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
2860
2686
  releaseVelocityX,
2861
2687
  releaseVelocityY
2862
2688
  }) {
2863
- const swipeReleasePayload = {
2864
- deltaX,
2865
- deltaY,
2866
- velocityX,
2867
- velocityY,
2868
- releaseVelocityX,
2869
- releaseVelocityY
2870
- };
2689
+ const popupElement = store.context.popupRef.current;
2690
+ if (!popupElement) {
2691
+ clearSwipeRelease();
2692
+ return undefined;
2693
+ }
2694
+ const releasePopupElement = popupElement;
2871
2695
  function startSwipeRelease(resolvedDirection) {
2872
2696
  // Start ending transition styles earlier and synchronously to prevent a period where
2873
2697
  // the popup appears stuck on release before the actual closing animation starts.
2874
- const popupElement = store.context.popupRef.current;
2875
- if (!popupElement) {
2876
- return;
2877
- }
2878
2698
  finishNestedSwipe();
2879
2699
  setSwipeDismissed(true);
2880
- popupElement.style.removeProperty('transition');
2881
- popupElement.setAttribute(TransitionStatusDataAttributes.endingStyle, '');
2700
+ releasePopupElement.style.removeProperty('transition');
2701
+ releasePopupElement.setAttribute(TransitionStatusDataAttributes.endingStyle, '');
2882
2702
  ReactDOM.flushSync(() => {
2883
- setSwipeRelease(resolveSwipeRelease({
2884
- direction: resolvedDirection,
2885
- ...swipeReleasePayload
2886
- }));
2703
+ setSwipeRelease(resolveSwipeRelease(releasePopupElement, resolvedDirection, deltaX, deltaY, velocityX, velocityY, releaseVelocityX, releaseVelocityY));
2887
2704
  });
2888
2705
  }
2889
2706
  if (!snapPoints || snapPoints.length === 0) {
@@ -2891,29 +2708,16 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
2891
2708
  clearSwipeRelease();
2892
2709
  return undefined;
2893
2710
  }
2894
- const element = store.context.popupRef.current;
2895
- if (!element) {
2896
- clearSwipeRelease();
2897
- return undefined;
2898
- }
2899
- const baseThreshold = getBaseSwipeThreshold(element, direction);
2900
- const delta = direction === 'left' || direction === 'right' ? deltaX : deltaY;
2901
- if (!Number.isFinite(delta)) {
2902
- clearSwipeRelease();
2903
- return undefined;
2904
- }
2905
- const directionalDelta = direction === 'left' || direction === 'up' ? -delta : delta;
2711
+ const directionalDelta = getDisplacement(direction, deltaX, deltaY);
2906
2712
  if (directionalDelta <= 0) {
2907
2713
  clearSwipeRelease();
2908
2714
  return false;
2909
2715
  }
2910
- const velocity = direction === 'left' || direction === 'right' ? velocityX : velocityY;
2911
- const directionalVelocity = direction === 'left' || direction === 'up' ? -velocity : velocity;
2912
- if (directionalVelocity >= FAST_SWIPE_VELOCITY && directionalDelta > 0) {
2716
+ if (getDisplacement(direction, velocityX, velocityY) >= FAST_SWIPE_VELOCITY) {
2913
2717
  startSwipeRelease(direction);
2914
2718
  return true;
2915
2719
  }
2916
- const shouldClose = directionalDelta > baseThreshold;
2720
+ const shouldClose = directionalDelta > getBaseSwipeThreshold(releasePopupElement, direction);
2917
2721
  if (shouldClose) {
2918
2722
  startSwipeRelease(direction);
2919
2723
  } else {
@@ -2925,20 +2729,20 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
2925
2729
  clearSwipeRelease();
2926
2730
  return undefined;
2927
2731
  }
2928
- if (!popupHeight || resolvedSnapPoints.length === 0) {
2732
+ if (!popupHeight) {
2929
2733
  clearSwipeRelease();
2930
- return undefined;
2734
+ return false;
2931
2735
  }
2932
- const dragDelta = swipeDirection === 'down' ? deltaY : -deltaY;
2933
- if (!Number.isFinite(dragDelta)) {
2736
+ if (resolvedSnapPoints.length === 0) {
2934
2737
  clearSwipeRelease();
2935
2738
  return undefined;
2936
2739
  }
2740
+ const dragDelta = swipeDirection === 'down' ? deltaY : -deltaY;
2937
2741
  const dragDirection = Math.sign(dragDelta);
2938
2742
  const releaseDirectionalVelocity = swipeDirection === 'down' ? releaseVelocityY : -releaseVelocityY;
2939
2743
  const fallbackDirectionalVelocity = swipeDirection === 'down' ? velocityY : -velocityY;
2940
- let resolvedDirectionalVelocity = Number.isFinite(releaseDirectionalVelocity) ? releaseDirectionalVelocity : fallbackDirectionalVelocity;
2941
- if (dragDirection !== 0 && Math.abs(dragDelta) >= MIN_SWIPE_THRESHOLD && Number.isFinite(resolvedDirectionalVelocity)) {
2744
+ let resolvedDirectionalVelocity = releaseDirectionalVelocity;
2745
+ if (dragDirection !== 0 && Math.abs(dragDelta) >= MIN_SWIPE_THRESHOLD) {
2942
2746
  const velocityDirection = Math.sign(resolvedDirectionalVelocity);
2943
2747
  if (velocityDirection !== 0 && velocityDirection !== dragDirection) {
2944
2748
  // Ignore touch reversals that would otherwise flip the snap decision.
@@ -2947,39 +2751,20 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
2947
2751
  }
2948
2752
  const currentOffset = activeSnapPointOffset ?? 0;
2949
2753
  const dragTargetOffset = clamp(currentOffset + dragDelta, 0, popupHeight);
2950
- const velocityOffset = Number.isFinite(resolvedDirectionalVelocity) && Math.abs(resolvedDirectionalVelocity) >= SNAP_VELOCITY_THRESHOLD ? clamp(resolvedDirectionalVelocity, -MAX_SNAP_VELOCITY, MAX_SNAP_VELOCITY) * SNAP_VELOCITY_MULTIPLIER : 0;
2754
+ const velocityOffset = Math.abs(resolvedDirectionalVelocity) >= SNAP_VELOCITY_THRESHOLD ? clamp(resolvedDirectionalVelocity, -MAX_SNAP_VELOCITY, MAX_SNAP_VELOCITY) * SNAP_VELOCITY_MULTIPLIER : 0;
2951
2755
  const targetOffset = snapToSequentialPoints ? dragTargetOffset : clamp(dragTargetOffset + velocityOffset, 0, popupHeight);
2952
2756
  const snapPointEventDetails = createChangeEventDetails(swipe, event);
2953
2757
  const closeFromSnapPoints = () => {
2954
2758
  pendingSwipeCloseSnapPointRef.current = activeSnapPoint;
2955
- setActiveSnapPoint?.(null, snapPointEventDetails);
2759
+ setActiveSnapPoint(null, snapPointEventDetails);
2956
2760
  startSwipeRelease(swipeDirection);
2957
2761
  return true;
2958
2762
  };
2959
2763
  if (snapToSequentialPoints) {
2960
2764
  const orderedSnapPoints = [...resolvedSnapPoints].sort((first, second) => first.offset - second.offset);
2961
- if (orderedSnapPoints.length === 0) {
2962
- clearSwipeRelease();
2963
- return false;
2964
- }
2965
- let currentIndex = 0;
2966
- let closestDistance = Math.abs(currentOffset - orderedSnapPoints[0].offset);
2967
- for (let index = 1; index < orderedSnapPoints.length; index += 1) {
2968
- const distance = Math.abs(currentOffset - orderedSnapPoints[index].offset);
2969
- if (distance < closestDistance) {
2970
- closestDistance = distance;
2971
- currentIndex = index;
2972
- }
2973
- }
2974
- let targetSnapPoint = orderedSnapPoints[0];
2975
- closestDistance = Math.abs(targetOffset - targetSnapPoint.offset);
2976
- for (const snapPoint of orderedSnapPoints) {
2977
- const distance = Math.abs(targetOffset - snapPoint.offset);
2978
- if (distance < closestDistance) {
2979
- closestDistance = distance;
2980
- targetSnapPoint = snapPoint;
2981
- }
2982
- }
2765
+ const orderedOffsets = orderedSnapPoints.map(point => point.offset);
2766
+ const currentIndex = closestSnapPointIndex(orderedOffsets, currentOffset);
2767
+ let targetSnapPoint = orderedSnapPoints[closestSnapPointIndex(orderedOffsets, targetOffset)];
2983
2768
  const velocityDirection = Math.sign(resolvedDirectionalVelocity);
2984
2769
  const shouldAdvance = dragDirection !== 0 && velocityDirection !== 0 && velocityDirection === dragDirection && Math.abs(resolvedDirectionalVelocity) >= SNAP_VELOCITY_THRESHOLD;
2985
2770
  let effectiveTargetOffset = targetOffset;
@@ -2996,34 +2781,24 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
2996
2781
  return closeFromSnapPoints();
2997
2782
  }
2998
2783
  }
2999
- const closeOffset = popupHeight;
3000
- const closeDistance = Math.abs(effectiveTargetOffset - closeOffset);
2784
+ const closeDistance = Math.abs(effectiveTargetOffset - popupHeight);
3001
2785
  const snapDistance = Math.abs(effectiveTargetOffset - targetSnapPoint.offset);
3002
2786
  if (closeDistance < snapDistance) {
3003
2787
  return closeFromSnapPoints();
3004
2788
  }
3005
- setActiveSnapPoint?.(targetSnapPoint.value, snapPointEventDetails);
2789
+ setActiveSnapPoint(targetSnapPoint.value, snapPointEventDetails);
3006
2790
  clearSwipeRelease();
3007
2791
  return false;
3008
2792
  }
3009
2793
  if (resolvedDirectionalVelocity >= FAST_SWIPE_VELOCITY && dragDelta > 0) {
3010
2794
  return closeFromSnapPoints();
3011
2795
  }
3012
- let closestSnapPoint = resolvedSnapPoints[0];
3013
- let closestDistance = Math.abs(targetOffset - closestSnapPoint.offset);
3014
- for (const snapPoint of resolvedSnapPoints) {
3015
- const distance = Math.abs(targetOffset - snapPoint.offset);
3016
- if (distance < closestDistance) {
3017
- closestDistance = distance;
3018
- closestSnapPoint = snapPoint;
3019
- }
3020
- }
3021
- const closeOffset = popupHeight;
3022
- const closeDistance = Math.abs(targetOffset - closeOffset);
3023
- if (closeDistance < closestDistance) {
2796
+ const closestSnapPoint = resolvedSnapPoints[closestSnapPointIndex(resolvedSnapPoints.map(point => point.offset), targetOffset)];
2797
+ const closeDistance = Math.abs(targetOffset - popupHeight);
2798
+ if (closeDistance < Math.abs(targetOffset - closestSnapPoint.offset)) {
3024
2799
  return closeFromSnapPoints();
3025
2800
  }
3026
- setActiveSnapPoint?.(closestSnapPoint.value, snapPointEventDetails);
2801
+ setActiveSnapPoint(closestSnapPoint.value, snapPointEventDetails);
3027
2802
  clearSwipeRelease();
3028
2803
  return false;
3029
2804
  },
@@ -3042,7 +2817,7 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
3042
2817
  if (dismissEventDetails.isCanceled) {
3043
2818
  const pendingSnapPoint = pendingSwipeCloseSnapPointRef.current;
3044
2819
  if (pendingSnapPoint !== undefined) {
3045
- setActiveSnapPoint?.(pendingSnapPoint, createChangeEventDetails(swipe, event));
2820
+ setActiveSnapPoint(pendingSnapPoint, createChangeEventDetails(swipe, event));
3046
2821
  }
3047
2822
  pendingSwipeCloseSnapPointRef.current = undefined;
3048
2823
  resetSwipeRef.current?.();
@@ -3064,7 +2839,7 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
3064
2839
  // Parent rejected: revert animation and restore snap point.
3065
2840
  const pendingSnapPoint = pendingSwipeCloseSnapPointRef.current;
3066
2841
  if (pendingSnapPoint !== undefined) {
3067
- setActiveSnapPoint?.(pendingSnapPoint, createChangeEventDetails(swipe, savedEvent));
2842
+ setActiveSnapPoint(pendingSnapPoint, createChangeEventDetails(swipe, savedEvent));
3068
2843
  }
3069
2844
  pendingSwipeCloseSnapPointRef.current = undefined;
3070
2845
  clearSwipeRelease();
@@ -3094,42 +2869,18 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
3094
2869
  }
3095
2870
  const resolvedRootElement = rootElement;
3096
2871
  const doc = ownerDocument(resolvedRootElement);
3097
- const win = getWindow(doc);
3098
- function handleNativeTouchMove(event) {
3099
- // The virtual keyboard provider observes the move to tell a tap apart from a drag.
3100
- // It must run even when the swipe gesture below claims the event with
3101
- // `stopPropagation()`, which would otherwise prevent React's delegated handlers
3102
- // (and the provider) from ever seeing the move.
3103
- virtualKeyboard?.onTouchMove(event);
3104
- if (ignoreTouchSwipeRef.current) {
3105
- return;
3106
- }
3107
- const touchState = touchScrollStateRef.current;
3108
- const touch = event.touches[0];
3109
- if (!touch || !touchState) {
3110
- return;
3111
- }
2872
+ function processTouchMove(event, touchState, touch) {
3112
2873
  const drawerAxisDelta = isVerticalScrollAxis ? touch.clientY - touchState.lastY : touch.clientX - touchState.lastX;
3113
2874
 
3114
- // Preserve native range interaction by never locking touchmove for range inputs.
3115
- if (isEventOnRangeInput(event, win)) {
3116
- touchState.allowSwipe = false;
3117
- updateTouchScrollPosition(touchState, touch);
3118
- return;
3119
- }
3120
-
3121
2875
  // Avoid blocking pinch zoom or text selection adjustments on iOS Safari.
3122
2876
  if (event.touches.length === 2) {
3123
- updateTouchScrollPosition(touchState, touch);
3124
2877
  return;
3125
2878
  }
3126
2879
  const allowTouchMove = shouldIgnoreSwipeForTextSelection(doc, resolvedRootElement);
3127
2880
  if (allowTouchMove || !open || !mounted || nestedDrawerOpen) {
3128
- updateTouchScrollPosition(touchState, touch);
3129
2881
  return;
3130
2882
  }
3131
- if (preserveNativeCrossAxisScrollOnMove(touchState, touch, isVerticalScrollAxis)) {
3132
- updateTouchScrollPosition(touchState, touch);
2883
+ if (shouldYieldTouchMove(touchState, event, touch, isVerticalScrollAxis)) {
3133
2884
  return;
3134
2885
  }
3135
2886
  const scrollTarget = touchState.scrollTarget;
@@ -3141,27 +2892,21 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
3141
2892
  // move through React re-rasterizes the popup content on every frame.
3142
2893
  event.stopPropagation();
3143
2894
  moveSwipeNative(event, resolvedRootElement);
3144
- updateTouchScrollPosition(touchState, touch);
3145
2895
  return;
3146
2896
  }
3147
- const hasScrollableContent = hasScrollableContentOnAxis(scrollTarget, scrollAxis);
3148
- if (!hasScrollableContent) {
2897
+ if (!hasScrollableContentOnAxis(scrollTarget, scrollAxis)) {
3149
2898
  // If the scroll container doesn't overflow on the drawer axis, prevent the window from
3150
2899
  // scrolling instead.
3151
2900
  if (event.cancelable) {
3152
2901
  event.preventDefault();
3153
2902
  }
3154
2903
  event.stopPropagation();
3155
- updateTouchScrollPosition(touchState, touch);
3156
2904
  return;
3157
2905
  }
3158
- const delta = drawerAxisDelta;
3159
- if (delta !== 0) {
3160
- const canSwipeFromScrollEdge = canSwipeFromScrollEdgeOnMove(scrollTarget, scrollAxis, swipeDirection, delta);
2906
+ if (drawerAxisDelta !== 0) {
2907
+ const canSwipeFromScrollEdge = canSwipeFromScrollEdgeOnMove(scrollTarget, scrollAxis, swipeDirection, drawerAxisDelta);
3161
2908
  if (!touchState.allowSwipe) {
3162
- if (!event.cancelable) {
3163
- touchState.allowSwipe = false;
3164
- } else if (canSwipeFromScrollEdge) {
2909
+ if (event.cancelable && canSwipeFromScrollEdge) {
3165
2910
  touchState.allowSwipe = true;
3166
2911
  event.preventDefault();
3167
2912
  } else {
@@ -3175,6 +2920,22 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
3175
2920
  event.stopPropagation();
3176
2921
  moveSwipeNative(event, resolvedRootElement);
3177
2922
  }
2923
+ }
2924
+ function handleNativeTouchMove(event) {
2925
+ // The virtual keyboard provider observes the move to tell a tap apart from a drag.
2926
+ // It must run even when the swipe gesture below claims the event with
2927
+ // `stopPropagation()`, which would otherwise prevent React's delegated handlers
2928
+ // (and the provider) from ever seeing the move.
2929
+ virtualKeyboard?.onTouchMove(event);
2930
+ if (ignoreTouchSwipeRef.current) {
2931
+ return;
2932
+ }
2933
+ const touchState = touchScrollStateRef.current;
2934
+ const touch = event.touches[0];
2935
+ if (!touch || !touchState) {
2936
+ return;
2937
+ }
2938
+ processTouchMove(event, touchState, touch);
3178
2939
  updateTouchScrollPosition(touchState, touch);
3179
2940
  }
3180
2941
  return addEventListener(doc, 'touchmove', handleNativeTouchMove, {
@@ -3182,18 +2943,13 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
3182
2943
  capture: true
3183
2944
  });
3184
2945
  }, [mounted, nestedDrawerOpen, open, popupElementState, isVerticalScrollAxis, scrollAxis, swipeDirection, moveSwipeNative, viewportElement, virtualKeyboard]);
3185
- React.useEffect(() => {
2946
+ useIsoLayoutEffect(() => {
3186
2947
  if (!snapPointRange || swipe$1.swiping) {
3187
2948
  return;
3188
2949
  }
3189
- const resolvedProgress = !open || nested ? 0 : snapPointProgress ?? 0;
3190
- applySwipeProgress({
3191
- resolvedProgress,
3192
- shouldTrackProgress: true,
3193
- notifyParent: false
3194
- });
2950
+ applySwipeProgress(!open || nested ? 0 : snapPointProgress ?? 0, true, false);
3195
2951
  }, [applySwipeProgress, frontmostHeight, nested, notifyParentSwipeProgressChange, open, snapPointProgress, snapPointRange, swipe$1.swiping, store, visualStateStore]);
3196
- React.useEffect(() => {
2952
+ useIsoLayoutEffect(() => {
3197
2953
  if (!notifyParentSwipeProgressChange) {
3198
2954
  return undefined;
3199
2955
  }
@@ -3204,13 +2960,19 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
3204
2960
  notifyParentSwipeProgressChange(0);
3205
2961
  };
3206
2962
  }, [notifyParentSwipeProgressChange, open]);
3207
- React.useEffect(() => {
2963
+ useIsoLayoutEffect(() => {
3208
2964
  if (open) {
3209
- resetSwipe();
2965
+ // Skip `resetSwipe` while `Drawer.SwipeArea` is driving the open: it zeroes the popup's
2966
+ // `--swipe-movement-*` (via `syncDragStyles(false)`), flashing it fully open for a frame.
2967
+ // `clearSwipeRelease` doesn't touch those vars, so always run it to clear any leftover
2968
+ // release state from a prior dismiss (e.g. when the popup is kept mounted).
2969
+ if (!swipeAreaActiveRef.current) {
2970
+ resetSwipe();
2971
+ }
3210
2972
  clearSwipeRelease();
3211
2973
  }
3212
- }, [clearSwipeRelease, open, resetSwipe]);
3213
- React.useEffect(() => {
2974
+ }, [clearSwipeRelease, open, resetSwipe, swipeAreaActiveRef]);
2975
+ useIsoLayoutEffect(() => {
3214
2976
  const backdropElement = backdropRef.current;
3215
2977
  return () => {
3216
2978
  visualStateStore?.set({
@@ -3235,12 +2997,19 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
3235
2997
  swipeStrength: swipeRelease ?? null,
3236
2998
  setSwipeDismissed
3237
2999
  }), [setSwipeDismissed, swipe$1.getDragStyles, swipe$1.swiping, swipeRelease]);
3238
- function resetTouchTrackingState() {
3239
- ignoreTouchSwipeRef.current = false;
3000
+ function resetTouchSwipeState(ignoreSwipe) {
3001
+ ignoreTouchSwipeRef.current = ignoreSwipe;
3240
3002
  touchScrollStateRef.current = null;
3003
+ }
3004
+ function resetTouchTrackingState() {
3005
+ resetTouchSwipeState(false);
3241
3006
  lastPointerTypeRef.current = '';
3242
3007
  ignoreNextTouchStartFromPenRef.current = false;
3243
3008
  }
3009
+ function handlePointerEnd(event) {
3010
+ lastPointerTypeRef.current = '';
3011
+ return event.pointerType !== 'touch';
3012
+ }
3244
3013
  return /*#__PURE__*/jsx(DialogViewport, {
3245
3014
  ref: forwardedRef,
3246
3015
  className: className,
@@ -3253,8 +3022,7 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
3253
3022
  if (!open || !mounted || nestedDrawerOpen) {
3254
3023
  return;
3255
3024
  }
3256
- const doc = ownerDocument(event.currentTarget);
3257
- const elementAtPoint = getElementAtPoint(doc, event.clientX, event.clientY);
3025
+ const elementAtPoint = getElementAtPoint(event.currentTarget.getRootNode(), event.clientX, event.clientY);
3258
3026
  if (isSwipeIgnoredTarget(elementAtPoint) || isDrawerContentTarget(elementAtPoint)) {
3259
3027
  return;
3260
3028
  }
@@ -3270,34 +3038,24 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
3270
3038
  swipePointerProps.onPointerMove?.(event);
3271
3039
  },
3272
3040
  onPointerUp(event) {
3273
- if (lastPointerTypeRef.current === event.pointerType) {
3274
- lastPointerTypeRef.current = '';
3275
- }
3276
- if (event.pointerType === 'touch') {
3277
- return;
3041
+ if (handlePointerEnd(event)) {
3042
+ swipePointerProps.onPointerUp?.(event);
3278
3043
  }
3279
- swipePointerProps.onPointerUp?.(event);
3280
3044
  },
3281
3045
  onPointerCancel(event) {
3282
- if (lastPointerTypeRef.current === event.pointerType) {
3283
- lastPointerTypeRef.current = '';
3284
- }
3285
- if (event.pointerType === 'touch') {
3286
- return;
3046
+ if (handlePointerEnd(event)) {
3047
+ swipePointerProps.onPointerCancel?.(event);
3287
3048
  }
3288
- swipePointerProps.onPointerCancel?.(event);
3289
3049
  },
3290
3050
  onTouchStart(event) {
3291
3051
  const startedFromPenPointerDown = lastPointerTypeRef.current === 'pen' && ignoreNextTouchStartFromPenRef.current;
3292
3052
  if (startedFromPenPointerDown) {
3293
3053
  ignoreNextTouchStartFromPenRef.current = false;
3294
- ignoreTouchSwipeRef.current = false;
3295
- touchScrollStateRef.current = null;
3054
+ resetTouchSwipeState(false);
3296
3055
  return;
3297
3056
  }
3298
3057
  if (!open || !mounted || nestedDrawerOpen) {
3299
- ignoreTouchSwipeRef.current = false;
3300
- touchScrollStateRef.current = null;
3058
+ resetTouchSwipeState(false);
3301
3059
  return;
3302
3060
  }
3303
3061
  const touch = event.touches[0];
@@ -3305,32 +3063,25 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
3305
3063
  return;
3306
3064
  }
3307
3065
  if (isReactTouchEventOnRangeInput(event)) {
3308
- ignoreTouchSwipeRef.current = false;
3309
- touchScrollStateRef.current = null;
3066
+ resetTouchSwipeState(false);
3310
3067
  return;
3311
3068
  }
3312
- const doc = ownerDocument(event.currentTarget);
3313
- const elementAtPoint = getElementAtPoint(doc, touch.clientX, touch.clientY);
3314
- const rootElement = viewportElement ?? popupElementState;
3069
+ const rootElement = event.currentTarget;
3070
+ const elementAtPoint = getElementAtPoint(rootElement.getRootNode(), touch.clientX, touch.clientY);
3315
3071
  const eventTarget = getTarget(event.nativeEvent);
3316
- const target = isElement(eventTarget) ? eventTarget : null;
3317
- if (rootElement && target && !contains(rootElement, target)) {
3318
- ignoreTouchSwipeRef.current = true;
3319
- touchScrollStateRef.current = null;
3072
+ const target = isElement(eventTarget) ? eventTarget : rootElement;
3073
+ if (!contains(rootElement, target)) {
3074
+ resetTouchSwipeState(true);
3320
3075
  return;
3321
3076
  }
3322
3077
  virtualKeyboard?.onTouchStart(event);
3323
- ignoreTouchSwipeRef.current = isSwipeIgnoredTarget(elementAtPoint);
3324
- if (ignoreTouchSwipeRef.current) {
3325
- touchScrollStateRef.current = null;
3078
+ if (isSwipeIgnoredTarget(elementAtPoint)) {
3079
+ resetTouchSwipeState(true);
3326
3080
  return;
3327
3081
  }
3328
- let scrollTarget = null;
3329
- let hasCrossAxisScrollableContent = false;
3330
- if (rootElement && target) {
3331
- scrollTarget = findScrollableTouchTarget(target, rootElement, scrollAxis);
3332
- hasCrossAxisScrollableContent = findScrollableTouchTarget(target, rootElement, crossScrollAxis) != null;
3333
- }
3082
+ ignoreTouchSwipeRef.current = false;
3083
+ const scrollTarget = findScrollableTouchTarget(target, rootElement, scrollAxis);
3084
+ const hasCrossAxisScrollableContent = findScrollableTouchTarget(target, rootElement, crossScrollAxis) != null;
3334
3085
  let allowSwipe = null;
3335
3086
  if (scrollTarget) {
3336
3087
  const canSwipeFromEdge = isAtSwipeStartEdge(scrollTarget, scrollAxis, swipeDirection);
@@ -3344,7 +3095,8 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
3344
3095
  scrollTarget,
3345
3096
  hasCrossAxisScrollableContent,
3346
3097
  allowSwipe,
3347
- preserveNativeCrossAxisScroll: false
3098
+ preserveNativeCrossAxisScroll: false,
3099
+ drawerAxisAttributed: false
3348
3100
  };
3349
3101
  swipeTouchProps.onTouchStart?.(event);
3350
3102
  },
@@ -3369,24 +3121,8 @@ const DrawerViewport = /*#__PURE__*/React.forwardRef(function DrawerViewport(pro
3369
3121
  });
3370
3122
  });
3371
3123
  if (process.env.NODE_ENV !== "production") DrawerViewport.displayName = "DrawerViewport";
3372
- function setSwipeDismissedElements(popupElement, backdropElement, dismissed) {
3373
- if (dismissed) {
3374
- popupElement?.setAttribute(DrawerPopupDataAttributes.swipeDismiss, '');
3375
- backdropElement?.setAttribute(DrawerPopupDataAttributes.swipeDismiss, '');
3376
- return;
3377
- }
3378
- popupElement?.removeAttribute(DrawerPopupDataAttributes.swipeDismiss);
3379
- backdropElement?.removeAttribute(DrawerPopupDataAttributes.swipeDismiss);
3380
- }
3381
3124
  function setBackdropSwipingAttribute(backdropElement, swiping) {
3382
- if (!backdropElement) {
3383
- return;
3384
- }
3385
- if (swiping) {
3386
- backdropElement.setAttribute(DrawerPopupDataAttributes.swiping, '');
3387
- return;
3388
- }
3389
- backdropElement.removeAttribute(DrawerPopupDataAttributes.swiping);
3125
+ backdropElement?.toggleAttribute(DrawerPopupDataAttributes.swiping, swiping);
3390
3126
  }
3391
3127
  function isSwipeIgnoredTarget(target) {
3392
3128
  return Boolean(target?.closest(BASE_UI_SWIPE_IGNORE_SELECTOR));
@@ -3394,17 +3130,16 @@ function isSwipeIgnoredTarget(target) {
3394
3130
  function isDrawerContentTarget(target) {
3395
3131
  return Boolean(target?.closest(DRAWER_CONTENT_SELECTOR));
3396
3132
  }
3133
+ function getBaseSwipeSize(element, direction) {
3134
+ return direction === 'left' || direction === 'right' ? element.offsetWidth : element.offsetHeight;
3135
+ }
3397
3136
  function getBaseSwipeThreshold(element, direction) {
3398
- const size = direction === 'left' || direction === 'right' ? element.offsetWidth : element.offsetHeight;
3399
- return Math.max(size * 0.5, MIN_SWIPE_THRESHOLD);
3137
+ return Math.max(getBaseSwipeSize(element, direction) * 0.5, MIN_SWIPE_THRESHOLD);
3400
3138
  }
3401
3139
  function isRangeInput(target, win) {
3402
3140
  return target instanceof win.HTMLInputElement && target.type === 'range';
3403
3141
  }
3404
3142
  function isTextSelectionControl(target) {
3405
- if (!isElement(target)) {
3406
- return false;
3407
- }
3408
3143
  return target.tagName === 'INPUT' || target.tagName === 'TEXTAREA';
3409
3144
  }
3410
3145
  function hasExpandedSelectionWithinTarget(selection, target) {
@@ -3414,8 +3149,7 @@ function hasExpandedSelectionWithinTarget(selection, target) {
3414
3149
  }
3415
3150
  function shouldIgnoreSwipeForTextSelection(doc, rootElement) {
3416
3151
  const activeEl = activeElement(doc);
3417
- const activeElementWithinRoot = Boolean(activeEl && contains(rootElement, activeEl));
3418
- if (activeElementWithinRoot && isTextSelectionControl(activeEl)) {
3152
+ if (activeEl && contains(rootElement, activeEl) && isTextSelectionControl(activeEl)) {
3419
3153
  const {
3420
3154
  selectionStart,
3421
3155
  selectionEnd
@@ -3431,11 +3165,7 @@ function shouldIgnoreSwipeForTextSelection(doc, rootElement) {
3431
3165
  return hasExpandedSelectionWithinTarget(selection, rootElement);
3432
3166
  }
3433
3167
  function isEventOnRangeInput(event, win) {
3434
- const composedPath = event.composedPath();
3435
- if (composedPath) {
3436
- return composedPath.some(pathTarget => isRangeInput(pathTarget, win));
3437
- }
3438
- return isRangeInput(getTarget(event), win);
3168
+ return event.composedPath().some(pathTarget => isRangeInput(pathTarget, win));
3439
3169
  }
3440
3170
  function isReactTouchEventOnRangeInput(event) {
3441
3171
  return isEventOnRangeInput(event.nativeEvent, getWindow(event.currentTarget));
@@ -3444,25 +3174,51 @@ function updateTouchScrollPosition(touchState, touch) {
3444
3174
  touchState.lastX = touch.clientX;
3445
3175
  touchState.lastY = touch.clientY;
3446
3176
  }
3447
- function preserveNativeCrossAxisScrollOnMove(touchState, touch, isVerticalScrollAxis) {
3177
+
3178
+ /**
3179
+ * Arbitrates a touchmove between the drawer swipe and a native cross-axis scroll.
3180
+ * Returns `true` when the move must be left alone — either because the cross axis already won the
3181
+ * gesture, or because neither axis has passed the slop yet and the gesture cannot be attributed.
3182
+ */
3183
+ function shouldYieldTouchMove(touchState, event, touch, isVerticalScrollAxis) {
3448
3184
  if (touchState.preserveNativeCrossAxisScroll) {
3449
3185
  return true;
3450
3186
  }
3451
- if (touchState.allowSwipe === true || !touchState.hasCrossAxisScrollableContent) {
3187
+
3188
+ // Attribution happens once per gesture. Re-arbitrating after the drawer axis has won would let
3189
+ // the pre-attribution branches below fire mid-drag (the slop is measured from the touch origin,
3190
+ // which is never re-baselined), freezing the popup and dropping `preventDefault()`.
3191
+ if (touchState.drawerAxisAttributed || touchState.allowSwipe === true || !touchState.hasCrossAxisScrollableContent) {
3452
3192
  return false;
3453
3193
  }
3194
+
3195
+ // A non-cancelable touchmove means the browser has already committed the gesture to a native
3196
+ // scroll; claiming it for the swipe would drag the popup alongside the scrolling content.
3197
+ if (!event.cancelable) {
3198
+ touchState.preserveNativeCrossAxisScroll = true;
3199
+ return true;
3200
+ }
3454
3201
  const drawerAxisGestureDelta = isVerticalScrollAxis ? touch.clientY - touchState.startY : touch.clientX - touchState.startX;
3455
3202
  const crossAxisGestureDelta = isVerticalScrollAxis ? touch.clientX - touchState.startX : touch.clientY - touchState.startY;
3456
3203
  const absDrawerAxisGestureDelta = Math.abs(drawerAxisGestureDelta);
3457
3204
  const absCrossAxisGestureDelta = Math.abs(crossAxisGestureDelta);
3458
- if (absCrossAxisGestureDelta < 6 || absCrossAxisGestureDelta <= absDrawerAxisGestureDelta + 2) {
3205
+ if (absCrossAxisGestureDelta >= AXIS_LOCK_SLOP && absCrossAxisGestureDelta > absDrawerAxisGestureDelta + AXIS_LOCK_BIAS) {
3206
+ touchState.preserveNativeCrossAxisScroll = true;
3207
+ return true;
3208
+ }
3209
+ if (absDrawerAxisGestureDelta >= AXIS_LOCK_SLOP) {
3210
+ touchState.drawerAxisAttributed = true;
3459
3211
  return false;
3460
3212
  }
3461
- touchState.preserveNativeCrossAxisScroll = true;
3213
+
3214
+ // Neither axis has traveled past the slop yet, so the gesture cannot be attributed. Leave the
3215
+ // event alone: on iOS, `preventDefault()` on the first cancelable touchmove cancels native
3216
+ // scrolling for the entire gesture, which would lock a cross-axis scroll that only passes the
3217
+ // slop on a later move.
3462
3218
  return true;
3463
3219
  }
3464
3220
  function hasScrollableContentOnAxis(scrollTarget, axis) {
3465
- return axis === 'vertical' ? scrollTarget.scrollHeight > scrollTarget.clientHeight : scrollTarget.scrollWidth > scrollTarget.clientWidth;
3221
+ return getScrollMetrics(scrollTarget, axis).max > 0;
3466
3222
  }
3467
3223
  function getScrollMetrics(scrollTarget, axis) {
3468
3224
  if (axis === 'vertical') {
@@ -3479,48 +3235,23 @@ function getScrollMetrics(scrollTarget, axis) {
3479
3235
  };
3480
3236
  }
3481
3237
  function isAtSwipeStartEdge(scrollTarget, axis, direction) {
3238
+ const dismissFromStartEdge = shouldDismissFromStartEdge(direction, axis);
3482
3239
  const {
3483
3240
  offset,
3484
3241
  max
3485
3242
  } = getScrollMetrics(scrollTarget, axis);
3486
- const dismissFromStartEdge = shouldDismissFromStartEdge(direction, axis);
3487
- if (dismissFromStartEdge === null) {
3488
- return false;
3489
- }
3490
3243
  return dismissFromStartEdge ? offset <= 0 : offset >= max;
3491
3244
  }
3492
3245
  function canSwipeFromScrollEdgeOnMove(scrollTarget, axis, direction, delta) {
3493
- const {
3494
- offset,
3495
- max
3496
- } = getScrollMetrics(scrollTarget, axis);
3497
3246
  const dismissFromStartEdge = shouldDismissFromStartEdge(direction, axis);
3498
- if (dismissFromStartEdge === null) {
3499
- return false;
3500
- }
3501
3247
  const movingTowardDismiss = dismissFromStartEdge ? delta > 0 : delta < 0;
3502
3248
  if (!movingTowardDismiss) {
3503
3249
  return false;
3504
3250
  }
3505
- return dismissFromStartEdge ? offset <= 0 : offset >= max;
3251
+ return isAtSwipeStartEdge(scrollTarget, axis, direction);
3506
3252
  }
3507
3253
  function shouldDismissFromStartEdge(direction, axis) {
3508
- if (axis === 'vertical') {
3509
- if (direction === 'down') {
3510
- return true;
3511
- }
3512
- if (direction === 'up') {
3513
- return false;
3514
- }
3515
- return null;
3516
- }
3517
- if (direction === 'right') {
3518
- return true;
3519
- }
3520
- if (direction === 'left') {
3521
- return false;
3522
- }
3523
- return null;
3254
+ return axis === 'vertical' ? direction === 'down' : direction === 'right';
3524
3255
  }
3525
3256
 
3526
3257
  const ScrollAreaRootContext = /*#__PURE__*/React.createContext(undefined);
@@ -3533,20 +3264,6 @@ function useScrollAreaRootContext() {
3533
3264
  return context;
3534
3265
  }
3535
3266
 
3536
- let ScrollAreaRootCssVars = /*#__PURE__*/function (ScrollAreaRootCssVars) {
3537
- /**
3538
- * The scroll area's corner height.
3539
- * @type {number}
3540
- */
3541
- ScrollAreaRootCssVars["scrollAreaCornerHeight"] = "--scroll-area-corner-height";
3542
- /**
3543
- * The scroll area's corner width.
3544
- * @type {number}
3545
- */
3546
- ScrollAreaRootCssVars["scrollAreaCornerWidth"] = "--scroll-area-corner-width";
3547
- return ScrollAreaRootCssVars;
3548
- }({});
3549
-
3550
3267
  const SCROLL_TIMEOUT = 500;
3551
3268
  const MIN_THUMB_SIZE = 16;
3552
3269
 
@@ -3555,108 +3272,28 @@ function getOffset(element, prop, axis) {
3555
3272
  return 0;
3556
3273
  }
3557
3274
  const styles = getComputedStyle(element);
3558
- const propAxis = axis === 'x' ? 'Inline' : 'Block';
3275
+ const key = `${prop}${axis === 'x' ? 'Inline' : 'Block'}`;
3276
+ const start = parseFloat(styles[`${key}Start`]);
3559
3277
 
3560
3278
  // Safari misreports `marginInlineEnd` in RTL.
3561
3279
  // We have to assume the start/end values are symmetrical, which is likely.
3562
3280
  if (axis === 'x' && prop === 'margin') {
3563
- return parseFloat(styles[`${prop}InlineStart`]) * 2;
3281
+ return start * 2;
3564
3282
  }
3565
- return parseFloat(styles[`${prop}${propAxis}Start`]) + parseFloat(styles[`${prop}${propAxis}End`]);
3283
+ return start + parseFloat(styles[`${key}End`]);
3566
3284
  }
3567
3285
 
3568
- let ScrollAreaScrollbarDataAttributes = /*#__PURE__*/function (ScrollAreaScrollbarDataAttributes) {
3569
- /**
3570
- * Indicates the orientation of the scrollbar.
3571
- * @type {'horizontal' | 'vertical'}
3572
- */
3573
- ScrollAreaScrollbarDataAttributes["orientation"] = "data-orientation";
3574
- /**
3575
- * Present when the pointer is over the scroll area.
3576
- */
3577
- ScrollAreaScrollbarDataAttributes["hovering"] = "data-hovering";
3578
- /**
3579
- * Present when the user scrolls inside the scroll area.
3580
- */
3581
- ScrollAreaScrollbarDataAttributes["scrolling"] = "data-scrolling";
3582
- /**
3583
- * Present when the scroll area content is wider than the viewport.
3584
- */
3585
- ScrollAreaScrollbarDataAttributes["hasOverflowX"] = "data-has-overflow-x";
3586
- /**
3587
- * Present when the scroll area content is taller than the viewport.
3588
- */
3589
- ScrollAreaScrollbarDataAttributes["hasOverflowY"] = "data-has-overflow-y";
3590
- /**
3591
- * Present when there is overflow on the horizontal start side.
3592
- */
3593
- ScrollAreaScrollbarDataAttributes["overflowXStart"] = "data-overflow-x-start";
3594
- /**
3595
- * Present when there is overflow on the horizontal end side.
3596
- */
3597
- ScrollAreaScrollbarDataAttributes["overflowXEnd"] = "data-overflow-x-end";
3598
- /**
3599
- * Present when there is overflow on the vertical start side.
3600
- */
3601
- ScrollAreaScrollbarDataAttributes["overflowYStart"] = "data-overflow-y-start";
3602
- /**
3603
- * Present when there is overflow on the vertical end side.
3604
- */
3605
- ScrollAreaScrollbarDataAttributes["overflowYEnd"] = "data-overflow-y-end";
3606
- return ScrollAreaScrollbarDataAttributes;
3607
- }({});
3608
-
3609
- let ScrollAreaRootDataAttributes = /*#__PURE__*/function (ScrollAreaRootDataAttributes) {
3610
- /**
3611
- * Present when the user scrolls inside the scroll area.
3612
- */
3613
- ScrollAreaRootDataAttributes["scrolling"] = "data-scrolling";
3614
- /**
3615
- * Present when the scroll area content is wider than the viewport.
3616
- */
3617
- ScrollAreaRootDataAttributes["hasOverflowX"] = "data-has-overflow-x";
3618
- /**
3619
- * Present when the scroll area content is taller than the viewport.
3620
- */
3621
- ScrollAreaRootDataAttributes["hasOverflowY"] = "data-has-overflow-y";
3622
- /**
3623
- * Present when there is overflow on the horizontal start side.
3624
- */
3625
- ScrollAreaRootDataAttributes["overflowXStart"] = "data-overflow-x-start";
3626
- /**
3627
- * Present when there is overflow on the horizontal end side.
3628
- */
3629
- ScrollAreaRootDataAttributes["overflowXEnd"] = "data-overflow-x-end";
3630
- /**
3631
- * Present when there is overflow on the vertical start side.
3632
- */
3633
- ScrollAreaRootDataAttributes["overflowYStart"] = "data-overflow-y-start";
3634
- /**
3635
- * Present when there is overflow on the vertical end side.
3636
- */
3637
- ScrollAreaRootDataAttributes["overflowYEnd"] = "data-overflow-y-end";
3638
- return ScrollAreaRootDataAttributes;
3639
- }({});
3640
-
3286
+ // Data-attribute strings inlined so `ScrollAreaRootDataAttributes` tree-shakes out.
3287
+ const attr = name => value => value ? {
3288
+ [name]: ''
3289
+ } : null;
3641
3290
  const scrollAreaStateAttributesMapping = {
3642
- hasOverflowX: value => value ? {
3643
- [ScrollAreaRootDataAttributes.hasOverflowX]: ''
3644
- } : null,
3645
- hasOverflowY: value => value ? {
3646
- [ScrollAreaRootDataAttributes.hasOverflowY]: ''
3647
- } : null,
3648
- overflowXStart: value => value ? {
3649
- [ScrollAreaRootDataAttributes.overflowXStart]: ''
3650
- } : null,
3651
- overflowXEnd: value => value ? {
3652
- [ScrollAreaRootDataAttributes.overflowXEnd]: ''
3653
- } : null,
3654
- overflowYStart: value => value ? {
3655
- [ScrollAreaRootDataAttributes.overflowYStart]: ''
3656
- } : null,
3657
- overflowYEnd: value => value ? {
3658
- [ScrollAreaRootDataAttributes.overflowYEnd]: ''
3659
- } : null,
3291
+ hasOverflowX: attr('data-has-overflow-x'),
3292
+ hasOverflowY: attr('data-has-overflow-y'),
3293
+ overflowXStart: attr('data-overflow-x-start'),
3294
+ overflowXEnd: attr('data-overflow-x-end'),
3295
+ overflowYStart: attr('data-overflow-y-start'),
3296
+ overflowYEnd: attr('data-overflow-y-end'),
3660
3297
  cornerHidden: () => null
3661
3298
  };
3662
3299
 
@@ -3722,99 +3359,145 @@ const ScrollAreaRoot = /*#__PURE__*/React.forwardRef(function ScrollAreaRoot(com
3722
3359
  const thumbYRef = React.useRef(null);
3723
3360
  const thumbXRef = React.useRef(null);
3724
3361
  const cornerRef = React.useRef(null);
3725
- const thumbDraggingRef = React.useRef(false);
3362
+ const activePointerIdRef = React.useRef(null);
3726
3363
  const startYRef = React.useRef(0);
3727
3364
  const startXRef = React.useRef(0);
3728
3365
  const startScrollTopRef = React.useRef(0);
3729
3366
  const startScrollLeftRef = React.useRef(0);
3730
3367
  const currentOrientationRef = React.useRef('vertical');
3731
3368
  const scrollPositionRef = React.useRef(DEFAULT_COORDS);
3369
+ const savedSnapTypeRef = React.useRef(null);
3370
+ function startScrolling(vertical) {
3371
+ const setScrolling = vertical ? setScrollingY : setScrollingX;
3372
+ const timeout = vertical ? scrollYTimeout : scrollXTimeout;
3373
+ setScrolling(true);
3374
+ timeout.start(SCROLL_TIMEOUT, () => {
3375
+ setScrolling(false);
3376
+ });
3377
+ }
3732
3378
  const handleScroll = useStableCallback(scrollPosition => {
3733
3379
  const offsetX = scrollPosition.x - scrollPositionRef.current.x;
3734
3380
  const offsetY = scrollPosition.y - scrollPositionRef.current.y;
3735
3381
  scrollPositionRef.current = scrollPosition;
3736
3382
  if (offsetY !== 0) {
3737
- setScrollingY(true);
3738
- scrollYTimeout.start(SCROLL_TIMEOUT, () => {
3739
- setScrollingY(false);
3740
- });
3383
+ startScrolling(true);
3741
3384
  }
3742
3385
  if (offsetX !== 0) {
3743
- setScrollingX(true);
3744
- scrollXTimeout.start(SCROLL_TIMEOUT, () => {
3745
- setScrollingX(false);
3746
- });
3386
+ startScrolling(false);
3387
+ }
3388
+ });
3389
+
3390
+ // CSS scroll snap forces every programmatic scroll to land on a snap
3391
+ // point, making thumb dragging jump between snap points. Native
3392
+ // scrollbars suppress snapping while dragging, so disable it until the
3393
+ // pointer is released; restoring the value re-snaps the viewport. The
3394
+ // save is guarded so a second pointer during an active drag can't
3395
+ // clobber the saved value with `none`.
3396
+ const disableViewportSnap = useStableCallback(() => {
3397
+ const viewportEl = viewportRef.current;
3398
+ if (viewportEl && savedSnapTypeRef.current === null) {
3399
+ savedSnapTypeRef.current = viewportEl.style.scrollSnapType;
3400
+ viewportEl.style.scrollSnapType = 'none';
3747
3401
  }
3748
3402
  });
3749
3403
  const handlePointerDown = useStableCallback(event => {
3750
3404
  if (event.button !== 0) {
3751
3405
  return;
3752
3406
  }
3753
- thumbDraggingRef.current = true;
3407
+ if (activePointerIdRef.current !== null) {
3408
+ const activeThumb = currentOrientationRef.current === 'vertical' ? thumbYRef.current : thumbXRef.current;
3409
+ // A live drag holds capture for the active pointer — ignore other pointers.
3410
+ // No capture means the release went missing entirely (silent capture drop
3411
+ // with an id that never reappears, e.g. a lost touch contact), so let the
3412
+ // new pointer take over the latch instead of leaving dragging dead.
3413
+ if (activeThumb?.hasPointerCapture(activePointerIdRef.current)) {
3414
+ return;
3415
+ }
3416
+ }
3417
+ activePointerIdRef.current = event.pointerId;
3754
3418
  startYRef.current = event.clientY;
3755
3419
  startXRef.current = event.clientX;
3756
- currentOrientationRef.current = event.currentTarget.getAttribute(ScrollAreaScrollbarDataAttributes.orientation);
3757
- if (viewportRef.current) {
3758
- startScrollTopRef.current = viewportRef.current.scrollTop;
3759
- startScrollLeftRef.current = viewportRef.current.scrollLeft;
3760
- }
3761
- if (thumbYRef.current && currentOrientationRef.current === 'vertical') {
3762
- thumbYRef.current.setPointerCapture(event.pointerId);
3763
- }
3764
- if (thumbXRef.current && currentOrientationRef.current === 'horizontal') {
3765
- thumbXRef.current.setPointerCapture(event.pointerId);
3420
+ // Literal instead of `ScrollAreaScrollbarDataAttributes.orientation`: referencing an
3421
+ // enum member retains its whole object in the bundle, so the strings are inlined and
3422
+ // the enums kept for docs only.
3423
+ currentOrientationRef.current = event.currentTarget.getAttribute('data-orientation');
3424
+ const viewportEl = viewportRef.current;
3425
+ if (viewportEl) {
3426
+ startScrollTopRef.current = viewportEl.scrollTop;
3427
+ startScrollLeftRef.current = viewportEl.scrollLeft;
3428
+ disableViewportSnap();
3766
3429
  }
3430
+ const thumb = currentOrientationRef.current === 'vertical' ? thumbYRef.current : thumbXRef.current;
3431
+ thumb?.setPointerCapture(event.pointerId);
3767
3432
  });
3768
- const handlePointerMove = useStableCallback(event => {
3769
- if (!thumbDraggingRef.current) {
3433
+ const handlePointerUp = useStableCallback(event => {
3434
+ if (event.pointerId !== activePointerIdRef.current) {
3770
3435
  return;
3771
3436
  }
3772
- const deltaY = event.clientY - startYRef.current;
3773
- const deltaX = event.clientX - startXRef.current;
3774
- if (viewportRef.current) {
3775
- const scrollableContentHeight = viewportRef.current.scrollHeight;
3776
- const viewportHeight = viewportRef.current.clientHeight;
3777
- const scrollableContentWidth = viewportRef.current.scrollWidth;
3778
- const viewportWidth = viewportRef.current.clientWidth;
3779
- if (thumbYRef.current && scrollbarYRef.current && currentOrientationRef.current === 'vertical') {
3780
- const scrollbarYOffset = getOffset(scrollbarYRef.current, 'padding', 'y');
3781
- const thumbYOffset = getOffset(thumbYRef.current, 'margin', 'y');
3782
- const thumbHeight = thumbYRef.current.offsetHeight;
3783
- const maxThumbOffsetY = scrollbarYRef.current.offsetHeight - thumbHeight - scrollbarYOffset - thumbYOffset;
3784
- const scrollRatioY = deltaY / maxThumbOffsetY;
3785
- viewportRef.current.scrollTop = startScrollTopRef.current + scrollRatioY * (scrollableContentHeight - viewportHeight);
3786
- event.preventDefault();
3787
- setScrollingY(true);
3788
- scrollYTimeout.start(SCROLL_TIMEOUT, () => {
3789
- setScrollingY(false);
3790
- });
3791
- }
3792
- if (thumbXRef.current && scrollbarXRef.current && currentOrientationRef.current === 'horizontal') {
3793
- const scrollbarXOffset = getOffset(scrollbarXRef.current, 'padding', 'x');
3794
- const thumbXOffset = getOffset(thumbXRef.current, 'margin', 'x');
3795
- const thumbWidth = thumbXRef.current.offsetWidth;
3796
- const maxThumbOffsetX = scrollbarXRef.current.offsetWidth - thumbWidth - scrollbarXOffset - thumbXOffset;
3797
- const scrollRatioX = deltaX / maxThumbOffsetX;
3798
- viewportRef.current.scrollLeft = startScrollLeftRef.current + scrollRatioX * (scrollableContentWidth - viewportWidth);
3799
- event.preventDefault();
3800
- setScrollingX(true);
3801
- scrollXTimeout.start(SCROLL_TIMEOUT, () => {
3802
- setScrollingX(false);
3803
- });
3437
+ activePointerIdRef.current = null;
3438
+ // Clear the drag's scrolling state immediately rather than waiting for the
3439
+ // `SCROLL_TIMEOUT` timer armed by the last drag move, so every release path
3440
+ // (real, `pointercancel`, or the missed-release fallback) behaves the same.
3441
+ (currentOrientationRef.current === 'vertical' ? setScrollingY : setScrollingX)(false);
3442
+ if (savedSnapTypeRef.current !== null) {
3443
+ if (viewportRef.current) {
3444
+ viewportRef.current.style.scrollSnapType = savedSnapTypeRef.current;
3804
3445
  }
3446
+ savedSnapTypeRef.current = null;
3805
3447
  }
3806
- });
3807
- const handlePointerUp = useStableCallback(event => {
3808
- thumbDraggingRef.current = false;
3809
-
3448
+ const thumb = currentOrientationRef.current === 'vertical' ? thumbYRef.current : thumbXRef.current;
3810
3449
  // `pointercancel` releases capture implicitly, so guard against releasing a
3811
3450
  // capture we no longer hold (which would throw).
3812
- if (thumbYRef.current && currentOrientationRef.current === 'vertical' && thumbYRef.current.hasPointerCapture(event.pointerId)) {
3813
- thumbYRef.current.releasePointerCapture(event.pointerId);
3451
+ if (thumb?.hasPointerCapture(event.pointerId)) {
3452
+ thumb.releasePointerCapture(event.pointerId);
3814
3453
  }
3815
- if (thumbXRef.current && currentOrientationRef.current === 'horizontal' && thumbXRef.current.hasPointerCapture(event.pointerId)) {
3816
- thumbXRef.current.releasePointerCapture(event.pointerId);
3454
+ });
3455
+ const handlePointerMove = useStableCallback(event => {
3456
+ if (event.pointerId !== activePointerIdRef.current) {
3457
+ return;
3458
+ }
3459
+
3460
+ // The release can go missing entirely (e.g. the browser drops pointer
3461
+ // capture while the scrollbar is hidden mid-drag), leaving the drag
3462
+ // latched so a buttonless hover over the thumb scrolls the viewport.
3463
+ // Treat a move without the primary button held (`buttons` bit 1 unset)
3464
+ // as the missed release.
3465
+ if (event.buttons % 2 === 0) {
3466
+ handlePointerUp(event);
3467
+ return;
3817
3468
  }
3469
+ const viewportEl = viewportRef.current;
3470
+ if (!viewportEl) {
3471
+ return;
3472
+ }
3473
+ const vertical = currentOrientationRef.current === 'vertical';
3474
+ const thumbEl = vertical ? thumbYRef.current : thumbXRef.current;
3475
+ const scrollbarEl = vertical ? scrollbarYRef.current : scrollbarXRef.current;
3476
+ if (!thumbEl || !scrollbarEl) {
3477
+ return;
3478
+ }
3479
+ const axis = vertical ? 'y' : 'x';
3480
+ const scrollbarOffset = getOffset(scrollbarEl, 'padding', axis);
3481
+ const thumbOffset = getOffset(thumbEl, 'margin', axis);
3482
+ const thumbSizePx = vertical ? thumbEl.offsetHeight : thumbEl.offsetWidth;
3483
+ const trackSize = vertical ? scrollbarEl.offsetHeight : scrollbarEl.offsetWidth;
3484
+ const maxThumbOffset = trackSize - thumbSizePx - scrollbarOffset - thumbOffset;
3485
+ // A short or heavily padded track can drive `maxThumbOffset` to zero or
3486
+ // negative once the thumb hits its `MIN_THUMB_SIZE` floor. Dividing by it
3487
+ // would yield a non-finite (`Infinity`/`NaN`) or inverted scroll position.
3488
+ const delta = vertical ? event.clientY - startYRef.current : event.clientX - startXRef.current;
3489
+ const scrollRatio = maxThumbOffset <= 0 ? 0 : delta / maxThumbOffset;
3490
+ const scrollableSize = vertical ? viewportEl.scrollHeight : viewportEl.scrollWidth;
3491
+ const viewportSize = vertical ? viewportEl.clientHeight : viewportEl.clientWidth;
3492
+ const startScroll = vertical ? startScrollTopRef.current : startScrollLeftRef.current;
3493
+ const nextScroll = startScroll + scrollRatio * (scrollableSize - viewportSize);
3494
+ if (vertical) {
3495
+ viewportEl.scrollTop = nextScroll;
3496
+ } else {
3497
+ viewportEl.scrollLeft = nextScroll;
3498
+ }
3499
+ event.preventDefault();
3500
+ startScrolling(vertical);
3818
3501
  });
3819
3502
  function handleTouchModalityChange(event) {
3820
3503
  setTouchModality(event.pointerType === 'touch');
@@ -3846,8 +3529,8 @@ const ScrollAreaRoot = /*#__PURE__*/React.forwardRef(function ScrollAreaRoot(com
3846
3529
  },
3847
3530
  style: {
3848
3531
  position: 'relative',
3849
- [ScrollAreaRootCssVars.scrollAreaCornerHeight]: `${cornerSize.height}px`,
3850
- [ScrollAreaRootCssVars.scrollAreaCornerWidth]: `${cornerSize.width}px`
3532
+ ['--scroll-area-corner-height']: `${cornerSize.height}px`,
3533
+ ['--scroll-area-corner-width']: `${cornerSize.width}px`
3851
3534
  }
3852
3535
  };
3853
3536
  const element = useRenderElement('div', componentProps, {
@@ -3861,6 +3544,7 @@ const ScrollAreaRoot = /*#__PURE__*/React.forwardRef(function ScrollAreaRoot(com
3861
3544
  handlePointerMove,
3862
3545
  handlePointerUp,
3863
3546
  handleScroll,
3547
+ disableViewportSnap,
3864
3548
  cornerSize,
3865
3549
  setCornerSize,
3866
3550
  thumbSize,
@@ -3870,13 +3554,10 @@ const ScrollAreaRoot = /*#__PURE__*/React.forwardRef(function ScrollAreaRoot(com
3870
3554
  touchModality,
3871
3555
  cornerRef,
3872
3556
  scrollingX,
3873
- setScrollingX,
3874
3557
  scrollingY,
3875
- setScrollingY,
3876
3558
  hovering,
3877
3559
  setHovering,
3878
3560
  viewportRef,
3879
- rootRef,
3880
3561
  scrollbarYRef,
3881
3562
  scrollbarXRef,
3882
3563
  thumbYRef,
@@ -3893,7 +3574,7 @@ const ScrollAreaRoot = /*#__PURE__*/React.forwardRef(function ScrollAreaRoot(com
3893
3574
  yStart,
3894
3575
  yEnd
3895
3576
  }
3896
- }), [handlePointerDown, handlePointerMove, handlePointerUp, handleScroll, cornerSize, thumbSize, hasMeasuredScrollbar, touchModality, scrollingX, setScrollingX, scrollingY, setScrollingY, hovering, setHovering, rootId, hiddenState, overflowEdges, state, xStart, xEnd, yStart, yEnd]);
3577
+ }), [handlePointerDown, handlePointerMove, handlePointerUp, handleScroll, disableViewportSnap, cornerSize, thumbSize, hasMeasuredScrollbar, touchModality, scrollingX, scrollingY, hovering, rootId, hiddenState, overflowEdges, state, xStart, xEnd, yStart, yEnd]);
3897
3578
  return /*#__PURE__*/jsxs(ScrollAreaRootContext.Provider, {
3898
3579
  value: contextValue,
3899
3580
  children: [!disableStyleElements && styleDisableScrollbar.getElement(nonce), element]
@@ -3901,20 +3582,17 @@ const ScrollAreaRoot = /*#__PURE__*/React.forwardRef(function ScrollAreaRoot(com
3901
3582
  });
3902
3583
  if (process.env.NODE_ENV !== "production") ScrollAreaRoot.displayName = "ScrollAreaRoot";
3903
3584
  function normalizeOverflowEdgeThreshold(threshold) {
3904
- if (typeof threshold === 'number') {
3905
- const value = Math.max(0, threshold);
3906
- return {
3907
- xStart: value,
3908
- xEnd: value,
3909
- yStart: value,
3910
- yEnd: value
3911
- };
3912
- }
3585
+ const thresholds = typeof threshold === 'number' ? {
3586
+ xStart: threshold,
3587
+ xEnd: threshold,
3588
+ yStart: threshold,
3589
+ yEnd: threshold
3590
+ } : threshold;
3913
3591
  return {
3914
- xStart: Math.max(0, threshold?.xStart || 0),
3915
- xEnd: Math.max(0, threshold?.xEnd || 0),
3916
- yStart: Math.max(0, threshold?.yStart || 0),
3917
- yEnd: Math.max(0, threshold?.yEnd || 0)
3592
+ xStart: Math.max(0, thresholds?.xStart || 0),
3593
+ xEnd: Math.max(0, thresholds?.xEnd || 0),
3594
+ yStart: Math.max(0, thresholds?.yStart || 0),
3595
+ yEnd: Math.max(0, thresholds?.yEnd || 0)
3918
3596
  };
3919
3597
  }
3920
3598
 
@@ -3928,30 +3606,10 @@ function useScrollAreaViewportContext() {
3928
3606
  return context;
3929
3607
  }
3930
3608
 
3931
- let ScrollAreaViewportCssVars = /*#__PURE__*/function (ScrollAreaViewportCssVars) {
3932
- /**
3933
- * The distance from the horizontal start edge in pixels.
3934
- * @type {number}
3935
- */
3936
- ScrollAreaViewportCssVars["scrollAreaOverflowXStart"] = "--scroll-area-overflow-x-start";
3937
- /**
3938
- * The distance from the horizontal end edge in pixels.
3939
- * @type {number}
3940
- */
3941
- ScrollAreaViewportCssVars["scrollAreaOverflowXEnd"] = "--scroll-area-overflow-x-end";
3942
- /**
3943
- * The distance from the vertical start edge in pixels.
3944
- * @type {number}
3945
- */
3946
- ScrollAreaViewportCssVars["scrollAreaOverflowYStart"] = "--scroll-area-overflow-y-start";
3947
- /**
3948
- * The distance from the vertical end edge in pixels.
3949
- * @type {number}
3950
- */
3951
- ScrollAreaViewportCssVars["scrollAreaOverflowYEnd"] = "--scroll-area-overflow-y-end";
3952
- return ScrollAreaViewportCssVars;
3953
- }({});
3609
+ const OVERFLOW_EDGE_VARS = ['--scroll-area-overflow-x-start', '--scroll-area-overflow-x-end', '--scroll-area-overflow-y-start', '--scroll-area-overflow-y-end'];
3954
3610
 
3611
+ // Module-level flag to ensure we only register the CSS properties once,
3612
+ // regardless of how many Scroll Area components are mounted.
3955
3613
  let scrollAreaOverflowVarsRegistered = false;
3956
3614
 
3957
3615
  /**
@@ -3970,7 +3628,7 @@ function removeCSSVariableInheritance() {
3970
3628
  return;
3971
3629
  }
3972
3630
  if (typeof CSS !== 'undefined' && 'registerProperty' in CSS) {
3973
- [ScrollAreaViewportCssVars.scrollAreaOverflowXStart, ScrollAreaViewportCssVars.scrollAreaOverflowXEnd, ScrollAreaViewportCssVars.scrollAreaOverflowYStart, ScrollAreaViewportCssVars.scrollAreaOverflowYEnd].forEach(name => {
3631
+ OVERFLOW_EDGE_VARS.forEach(name => {
3974
3632
  try {
3975
3633
  CSS.registerProperty({
3976
3634
  name,
@@ -4014,12 +3672,11 @@ const ScrollAreaViewport = /*#__PURE__*/React.forwardRef(function ScrollAreaView
4014
3672
  hiddenState,
4015
3673
  setHasMeasuredScrollbar,
4016
3674
  handleScroll,
3675
+ touchModality,
4017
3676
  setHovering,
4018
3677
  setOverflowEdges,
4019
- overflowEdges,
4020
3678
  overflowEdgeThreshold,
4021
- scrollingX,
4022
- scrollingY
3679
+ viewportState
4023
3680
  } = useScrollAreaRootContext();
4024
3681
  const direction = useDirection();
4025
3682
  const programmaticScrollRef = React.useRef(true);
@@ -4064,18 +3721,12 @@ const ScrollAreaViewport = /*#__PURE__*/React.forwardRef(function ScrollAreaView
4064
3721
  let scrollLeftFromStart = 0;
4065
3722
  let scrollLeftFromEnd = 0;
4066
3723
  if (!scrollbarXHidden) {
4067
- let rawScrollLeftFromStart = 0;
4068
- if (direction === 'rtl') {
4069
- rawScrollLeftFromStart = clamp(-scrollLeft, 0, maxScrollLeft);
4070
- } else {
4071
- rawScrollLeftFromStart = clamp(scrollLeft, 0, maxScrollLeft);
4072
- }
4073
- scrollLeftFromStart = normalizeScrollOffset(rawScrollLeftFromStart, maxScrollLeft);
3724
+ // `normalizeScrollOffset` clamps internally.
3725
+ scrollLeftFromStart = normalizeScrollOffset(direction === 'rtl' ? -scrollLeft : scrollLeft, maxScrollLeft);
4074
3726
  scrollLeftFromEnd = maxScrollLeft - scrollLeftFromStart;
4075
3727
  }
4076
- const rawScrollTopFromStart = !scrollbarYHidden ? clamp(scrollTop, 0, maxScrollTop) : 0;
4077
- const scrollTopFromStart = !scrollbarYHidden ? normalizeScrollOffset(rawScrollTopFromStart, maxScrollTop) : 0;
4078
- const scrollTopFromEnd = !scrollbarYHidden ? maxScrollTop - scrollTopFromStart : 0;
3728
+ const scrollTopFromStart = scrollbarYHidden ? 0 : normalizeScrollOffset(scrollTop, maxScrollTop);
3729
+ const scrollTopFromEnd = scrollbarYHidden ? 0 : maxScrollTop - scrollTopFromStart;
4079
3730
  const nextWidth = scrollbarXHidden ? 0 : viewportWidth;
4080
3731
  const nextHeight = scrollbarYHidden ? 0 : viewportHeight;
4081
3732
  let nextCornerWidth = 0;
@@ -4100,76 +3751,53 @@ const ScrollAreaViewport = /*#__PURE__*/React.forwardRef(function ScrollAreaView
4100
3751
  const maxNextHeight = scrollbarYEl ? Math.min(scrollbarYEl.offsetHeight - cornerHeightOffset, idealNextHeight) : idealNextHeight;
4101
3752
  const clampedNextWidth = Math.max(MIN_THUMB_SIZE, maxNextWidth * ratioX);
4102
3753
  const clampedNextHeight = Math.max(MIN_THUMB_SIZE, maxNextHeight * ratioY);
4103
- setThumbSize(prevSize => {
4104
- if (prevSize.height === clampedNextHeight && prevSize.width === clampedNextWidth) {
4105
- return prevSize;
4106
- }
4107
- return {
4108
- width: clampedNextWidth,
4109
- height: clampedNextHeight
4110
- };
4111
- });
3754
+ setThumbSize(prevSize => pickState(prevSize, {
3755
+ width: clampedNextWidth,
3756
+ height: clampedNextHeight
3757
+ }));
4112
3758
 
4113
3759
  // Handle Y (vertical) scroll
4114
3760
  if (scrollbarYEl && thumbYEl) {
4115
3761
  const maxThumbOffsetY = scrollbarYEl.offsetHeight - clampedNextHeight - scrollbarYOffset - thumbYOffset;
4116
- const scrollRangeY = scrollableContentHeight - viewportHeight;
4117
- const scrollRatioY = scrollRangeY === 0 ? 0 : scrollTop / scrollRangeY;
4118
-
4119
- // In Safari, don't allow it to go negative or too far as `scrollTop` considers the rubber
4120
- // band effect.
4121
- const thumbOffsetY = Math.min(maxThumbOffsetY, Math.max(0, scrollRatioY * maxThumbOffsetY));
3762
+ const thumbOffsetY = applyOverscrollThumb(thumbYEl, '--scroll-area-thumb-height', scrollTop, maxScrollTop, scrollableContentHeight, clampedNextHeight, maxThumbOffsetY);
4122
3763
  thumbYEl.style.transform = `translate3d(0,${thumbOffsetY}px,0)`;
4123
3764
  }
4124
3765
 
4125
3766
  // Handle X (horizontal) scroll
4126
3767
  if (scrollbarXEl && thumbXEl) {
4127
3768
  const maxThumbOffsetX = scrollbarXEl.offsetWidth - clampedNextWidth - scrollbarXOffset - thumbXOffset;
4128
- const scrollRangeX = scrollableContentWidth - viewportWidth;
4129
- const scrollRatioX = scrollRangeX === 0 ? 0 : scrollLeft / scrollRangeX;
4130
-
4131
- // In Safari, don't allow it to go negative or too far as `scrollLeft` considers the rubber
4132
- // band effect.
4133
- const thumbOffsetX = direction === 'rtl' ? clamp(scrollRatioX * maxThumbOffsetX, -maxThumbOffsetX, 0) : clamp(scrollRatioX * maxThumbOffsetX, 0, maxThumbOffsetX);
4134
- thumbXEl.style.transform = `translate3d(${thumbOffsetX}px,0,0)`;
4135
- }
4136
- const overflowMetricsPx = [[ScrollAreaViewportCssVars.scrollAreaOverflowXStart, scrollLeftFromStart], [ScrollAreaViewportCssVars.scrollAreaOverflowXEnd, scrollLeftFromEnd], [ScrollAreaViewportCssVars.scrollAreaOverflowYStart, scrollTopFromStart], [ScrollAreaViewportCssVars.scrollAreaOverflowYEnd, scrollTopFromEnd]];
4137
- for (const [cssVar, value] of overflowMetricsPx) {
4138
- viewportEl.style.setProperty(cssVar, `${value}px`);
4139
- }
3769
+ // RTL scrolls from 0 down to `-maxScrollLeft`; measure from the inline start edge so the
3770
+ // overscroll math is direction-agnostic, then flip the resulting offset back below.
3771
+ const scrollFromStart = direction === 'rtl' ? -scrollLeft : scrollLeft;
3772
+ const offsetX = applyOverscrollThumb(thumbXEl, '--scroll-area-thumb-width', scrollFromStart, maxScrollLeft, scrollableContentWidth, clampedNextWidth, maxThumbOffsetX);
3773
+ thumbXEl.style.transform = `translate3d(${direction === 'rtl' ? -offsetX : offsetX}px,0,0)`;
3774
+ }
3775
+ const overflowMetricsPx = [scrollLeftFromStart, scrollLeftFromEnd, scrollTopFromStart, scrollTopFromEnd];
3776
+ OVERFLOW_EDGE_VARS.forEach((cssVar, index) => {
3777
+ viewportEl.style.setProperty(cssVar, `${overflowMetricsPx[index]}px`);
3778
+ });
4140
3779
  if (cornerEl) {
4141
- if (scrollbarXHidden || scrollbarYHidden) {
4142
- setCornerSize({
4143
- width: 0,
4144
- height: 0
4145
- });
4146
- } else if (!scrollbarXHidden && !scrollbarYHidden) {
4147
- setCornerSize({
4148
- width: nextCornerWidth,
4149
- height: nextCornerHeight
4150
- });
4151
- }
4152
- }
4153
- setHiddenState(prevState => mergeHiddenState(prevState, nextHiddenState));
3780
+ // Bail when the size is unchanged (like `setThumbSize` above); otherwise a
3781
+ // fresh object literal on every scroll frame rebuilds the root context and
3782
+ // re-renders every scroll-area part.
3783
+ // `nextCornerWidth`/`nextCornerHeight` stay 0 when either scrollbar is hidden.
3784
+ setCornerSize(prevSize => pickState(prevSize, {
3785
+ width: nextCornerWidth,
3786
+ height: nextCornerHeight
3787
+ }));
3788
+ }
3789
+ setHiddenState(prevState => pickState(prevState, nextHiddenState));
4154
3790
  const nextOverflowEdges = {
4155
3791
  xStart: !scrollbarXHidden && scrollLeftFromStart > overflowEdgeThreshold.xStart,
4156
3792
  xEnd: !scrollbarXHidden && scrollLeftFromEnd > overflowEdgeThreshold.xEnd,
4157
3793
  yStart: !scrollbarYHidden && scrollTopFromStart > overflowEdgeThreshold.yStart,
4158
3794
  yEnd: !scrollbarYHidden && scrollTopFromEnd > overflowEdgeThreshold.yEnd
4159
3795
  };
4160
- setOverflowEdges(prev => {
4161
- if (prev.xStart === nextOverflowEdges.xStart && prev.xEnd === nextOverflowEdges.xEnd && prev.yStart === nextOverflowEdges.yStart && prev.yEnd === nextOverflowEdges.yEnd) {
4162
- return prev;
4163
- }
4164
- return nextOverflowEdges;
4165
- });
3796
+ setOverflowEdges(prev => pickState(prev, nextOverflowEdges));
4166
3797
  });
4167
3798
  useIsoLayoutEffect(() => {
4168
- if (!viewportRef.current) {
4169
- return;
4170
- }
4171
3799
  removeCSSVariableInheritance();
4172
- }, [viewportRef]);
3800
+ }, []);
4173
3801
  useIsoLayoutEffect(() => {
4174
3802
  // Wait for scrollbar and thumb refs after hidden-state toggles, refresh math on direction
4175
3803
  // flips, and re-evaluate overflow edges when the threshold changes.
@@ -4212,6 +3840,10 @@ const ScrollAreaViewport = /*#__PURE__*/React.forwardRef(function ScrollAreaView
4212
3840
  if (animations.length === 0) {
4213
3841
  return;
4214
3842
  }
3843
+
3844
+ // `allSettled` never rejects, but `computeThumbPosition` can still run against a
3845
+ // torn-down tree once the animations resolve. Swallow instead of leaking an unhandled
3846
+ // rejection; `void` alone would only silence the floating-promise lint.
4215
3847
  Promise.allSettled(animations.map(animation => animation.finished)).then(computeThumbPosition).catch(() => {});
4216
3848
  });
4217
3849
  return () => {
@@ -4239,7 +3871,13 @@ const ScrollAreaViewport = /*#__PURE__*/React.forwardRef(function ScrollAreaView
4239
3871
  return;
4240
3872
  }
4241
3873
  computeThumbPosition();
4242
- if (!programmaticScrollRef.current) {
3874
+
3875
+ // WebKit consumes a touch that catches an in-flight momentum scroll or
3876
+ // rubber-band bounce without dispatching any DOM events for the whole
3877
+ // gesture (not even `touchstart`), so scrolls cannot be attributed to
3878
+ // the user through events. Treat every scroll in touch modality as
3879
+ // user-driven instead.
3880
+ if (touchModality || !programmaticScrollRef.current) {
4243
3881
  handleScroll({
4244
3882
  x: viewportRef.current.scrollLeft,
4245
3883
  y: viewportRef.current.scrollTop
@@ -4257,21 +3895,10 @@ const ScrollAreaViewport = /*#__PURE__*/React.forwardRef(function ScrollAreaView
4257
3895
  });
4258
3896
  },
4259
3897
  onWheel: handleUserInteraction,
4260
- onTouchMove: handleUserInteraction,
4261
3898
  onPointerMove: handleUserInteraction,
4262
3899
  onPointerEnter: handleUserInteraction,
4263
3900
  onKeyDown: handleUserInteraction
4264
3901
  };
4265
- const viewportState = React.useMemo(() => ({
4266
- scrolling: scrollingX || scrollingY,
4267
- hasOverflowX: !hiddenState.x,
4268
- hasOverflowY: !hiddenState.y,
4269
- overflowXStart: overflowEdges.xStart,
4270
- overflowXEnd: overflowEdges.xEnd,
4271
- overflowYStart: overflowEdges.yStart,
4272
- overflowYEnd: overflowEdges.yEnd,
4273
- cornerHidden: hiddenState.corner
4274
- }), [scrollingX, scrollingY, hiddenState.x, hiddenState.y, hiddenState.corner, overflowEdges]);
4275
3902
  const element = useRenderElement('div', componentProps, {
4276
3903
  ref: [forwardedRef, viewportRef],
4277
3904
  state: viewportState,
@@ -4296,11 +3923,37 @@ function getHiddenState(viewport) {
4296
3923
  corner: y || x
4297
3924
  };
4298
3925
  }
4299
- function mergeHiddenState(prevState, nextState) {
4300
- if (prevState.y === nextState.y && prevState.x === nextState.x && prevState.corner === nextState.corner) {
4301
- return prevState;
3926
+
3927
+ /**
3928
+ * Returns `prev` when `next` is shallow-equal to it so setState bails out and
3929
+ * scroll-frame updates don't rebuild the root context.
3930
+ */
3931
+ function pickState(prev, next) {
3932
+ for (const key in next) {
3933
+ if (prev[key] !== next[key]) {
3934
+ return next;
3935
+ }
4302
3936
  }
4303
- return nextState;
3937
+ return prev;
3938
+ }
3939
+
3940
+ /**
3941
+ * Sizes the thumb and returns its axis offset. On overscroll (Safari rubber-band only) it shrinks
3942
+ * against the pinned edge, damped by `content / (content + overscroll)` to match native feedback;
3943
+ * the size flows through the thumb-size variable so the resting `var(...)` still applies.
3944
+ */
3945
+ function applyOverscrollThumb(thumbEl, sizeVar, scrollFromStart, maxScroll, content, size, maxThumbOffset) {
3946
+ const clamped = clamp(scrollFromStart, 0, maxScroll);
3947
+ const overscroll = scrollFromStart - clamped;
3948
+ const nextSize = Math.max(MIN_THUMB_SIZE, size * content / (content + Math.abs(overscroll)));
3949
+
3950
+ // Passing an empty string removes the override, restoring the resting `var(...)` size.
3951
+ thumbEl.style.setProperty(sizeVar, overscroll ? `${nextSize}px` : '');
3952
+
3953
+ // Slide proportionally; at the end edge push down by the shrink so the thumb stays pinned to
3954
+ // it, while a start overscroll pins to offset 0.
3955
+ const offset = maxScroll ? clamped / maxScroll * maxThumbOffset : 0;
3956
+ return offset + (overscroll > 0 ? size - nextSize : 0);
4304
3957
  }
4305
3958
 
4306
- export { getElementAtPoint as A, findScrollableTouchTarget as B, useScrollAreaRootContext as C, DrawerTrigger as D, getOffset as E, ScrollAreaRootCssVars as F, scrollAreaStateAttributesMapping as G, useScrollAreaViewportContext as H, ScrollAreaRoot as S, DrawerRoot as a, DrawerPortal as b, DrawerBackdrop as c, DrawerViewport as d, DrawerPopup as e, ScrollAreaViewport as f, DrawerContent as g, DialogRoot as h, DialogTrigger as i, DialogPortal as j, DialogViewport as k, useDialogPortalContext as l, DialogStore as m, createInitialPopupStoreState as n, useDrawerProviderContext as o, popupStoreSelectors as p, DrawerBackdropCssVars as q, DrawerPopupCssVars as r, DrawerProviderContext as s, useDrawerRootContext as t, useDialogRootContext as u, useSwipeDismiss as v, getDisplacement as w, DrawerPopupDataAttributes as x, getElementTransform as y, DrawerVirtualKeyboardContext as z };
3959
+ export { getElementTransform as A, DrawerVirtualKeyboardContext as B, getElementAtPoint as C, DrawerTrigger as D, useScrollAreaRootContext as E, getOffset as F, scrollAreaStateAttributesMapping as G, useScrollAreaViewportContext as H, NullStore as N, ScrollAreaRoot as S, DrawerRoot as a, DrawerPortal as b, DrawerBackdrop as c, DrawerViewport as d, DrawerPopup as e, ScrollAreaViewport as f, DrawerContent as g, DialogTrigger as h, DialogPortal as i, DialogViewport as j, useDialogPortalContext as k, dialogStateAttributesMapping as l, useRenderDialogRoot as m, createNullDialogStore as n, createInitialPopupStoreState as o, popupStoreSelectors as p, usePopupHandleStore as q, useDrawerProviderContext as r, DrawerBackdropCssVars as s, DrawerPopupCssVars as t, useDialogRootContext as u, DrawerProviderContext as v, useDrawerRootContext as w, useSwipeDismiss as x, getDisplacement as y, DrawerPopupDataAttributes as z };