@octanejs/base-ui 0.1.11 → 0.1.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,448 @@
1
+ // Ported verbatim from .base-ui/packages/react/src/floating-ui-react/safePolygon.ts (v1.6.0).
2
+ // Builds the "safe" triangle between the cursor's exit point and the popup so the user can move
3
+ // diagonally toward the popup without it closing under the pointer. Pure geometry plus a mousemove
4
+ // handler; no React/octane API is involved, so this is a straight transcription.
5
+ import { isElement } from '../dom';
6
+ import { Timeout } from '../useTimeout';
7
+ import { contains, getTarget } from './element';
8
+ import { getNodeChildren } from './nodes';
9
+ import type { HandleClose, HandleCloseOptions } from './useHoverShared';
10
+
11
+ const CURSOR_SPEED_THRESHOLD = 0.1;
12
+ const CURSOR_SPEED_THRESHOLD_SQUARED = CURSOR_SPEED_THRESHOLD * CURSOR_SPEED_THRESHOLD;
13
+ const POLYGON_BUFFER = 0.5;
14
+
15
+ interface Rect {
16
+ x: number;
17
+ y: number;
18
+ width: number;
19
+ height: number;
20
+ }
21
+
22
+ type Side = 'top' | 'right' | 'bottom' | 'left';
23
+
24
+ function hasIntersectingEdge(
25
+ pointX: number,
26
+ pointY: number,
27
+ xi: number,
28
+ yi: number,
29
+ xj: number,
30
+ yj: number,
31
+ ): boolean {
32
+ return yi >= pointY !== yj >= pointY && pointX <= ((xj - xi) * (pointY - yi)) / (yj - yi) + xi;
33
+ }
34
+
35
+ function isPointInQuadrilateral(
36
+ pointX: number,
37
+ pointY: number,
38
+ x1: number,
39
+ y1: number,
40
+ x2: number,
41
+ y2: number,
42
+ x3: number,
43
+ y3: number,
44
+ x4: number,
45
+ y4: number,
46
+ ): boolean {
47
+ let isInsideValue = false;
48
+
49
+ if (hasIntersectingEdge(pointX, pointY, x1, y1, x2, y2)) {
50
+ isInsideValue = !isInsideValue;
51
+ }
52
+ if (hasIntersectingEdge(pointX, pointY, x2, y2, x3, y3)) {
53
+ isInsideValue = !isInsideValue;
54
+ }
55
+ if (hasIntersectingEdge(pointX, pointY, x3, y3, x4, y4)) {
56
+ isInsideValue = !isInsideValue;
57
+ }
58
+ if (hasIntersectingEdge(pointX, pointY, x4, y4, x1, y1)) {
59
+ isInsideValue = !isInsideValue;
60
+ }
61
+
62
+ return isInsideValue;
63
+ }
64
+
65
+ function isInsideRect(pointX: number, pointY: number, rect: Rect): boolean {
66
+ return (
67
+ pointX >= rect.x &&
68
+ pointX <= rect.x + rect.width &&
69
+ pointY >= rect.y &&
70
+ pointY <= rect.y + rect.height
71
+ );
72
+ }
73
+
74
+ function isInsideAxisAlignedRect(
75
+ pointX: number,
76
+ pointY: number,
77
+ x1: number,
78
+ y1: number,
79
+ x2: number,
80
+ y2: number,
81
+ ): boolean {
82
+ const minX = Math.min(x1, x2);
83
+ const maxX = Math.max(x1, x2);
84
+ const minY = Math.min(y1, y2);
85
+ const maxY = Math.max(y1, y2);
86
+
87
+ return pointX >= minX && pointX <= maxX && pointY >= minY && pointY <= maxY;
88
+ }
89
+
90
+ export interface SafePolygonOptions extends HandleCloseOptions {}
91
+
92
+ /**
93
+ * Generates a safe polygon area that the user can traverse without closing the floating element
94
+ * once leaving the reference element.
95
+ */
96
+ export function safePolygon(options: SafePolygonOptions = {}): HandleClose {
97
+ const { blockPointerEvents = false } = options;
98
+ const timeout = new Timeout();
99
+
100
+ const fn: HandleClose = ({ x, y, placement, elements, onClose, nodeId, tree }) => {
101
+ const side = placement?.split('-')[0] as Side | undefined;
102
+ let hasLanded = false;
103
+ let lastX: number | null = null;
104
+ let lastY: number | null = null;
105
+ let lastCursorTime = typeof performance !== 'undefined' ? performance.now() : 0;
106
+
107
+ function isCursorMovingSlowly(nextX: number, nextY: number): boolean {
108
+ const currentTime = performance.now();
109
+ const elapsedTime = currentTime - lastCursorTime;
110
+
111
+ if (lastX === null || lastY === null || elapsedTime === 0) {
112
+ lastX = nextX;
113
+ lastY = nextY;
114
+ lastCursorTime = currentTime;
115
+ return false;
116
+ }
117
+
118
+ const deltaX = nextX - lastX;
119
+ const deltaY = nextY - lastY;
120
+ const distanceSquared = deltaX * deltaX + deltaY * deltaY;
121
+ const thresholdSquared = elapsedTime * elapsedTime * CURSOR_SPEED_THRESHOLD_SQUARED;
122
+
123
+ lastX = nextX;
124
+ lastY = nextY;
125
+ lastCursorTime = currentTime;
126
+
127
+ return distanceSquared < thresholdSquared;
128
+ }
129
+
130
+ function close(): void {
131
+ timeout.clear();
132
+ onClose();
133
+ }
134
+
135
+ return function onMouseMove(event: MouseEvent): undefined {
136
+ timeout.clear();
137
+
138
+ const domReference = elements.domReference;
139
+ const floating = elements.floating;
140
+ if (!domReference || !floating || side == null || x == null || y == null) {
141
+ return undefined;
142
+ }
143
+
144
+ const { clientX, clientY } = event;
145
+ const target = getTarget(event) as Element | null;
146
+ const isLeave = event.type === 'mouseleave';
147
+ const isOverFloatingEl = contains(floating, target);
148
+ const isOverReferenceEl = contains(domReference, target);
149
+
150
+ if (isOverFloatingEl) {
151
+ hasLanded = true;
152
+ if (!isLeave) {
153
+ return undefined;
154
+ }
155
+ }
156
+
157
+ if (isOverReferenceEl) {
158
+ hasLanded = false;
159
+ if (!isLeave) {
160
+ hasLanded = true;
161
+ return undefined;
162
+ }
163
+ }
164
+
165
+ // Prevent an overlapping floating element from getting stuck in an open-close loop.
166
+ if (isLeave && isElement(event.relatedTarget) && contains(floating, event.relatedTarget)) {
167
+ return undefined;
168
+ }
169
+
170
+ function hasOpenChildNode(): boolean {
171
+ return Boolean(tree && getNodeChildren(tree.nodesRef.current, nodeId).length > 0);
172
+ }
173
+
174
+ function closeIfNoOpenChild(): void {
175
+ if (!hasOpenChildNode()) {
176
+ close();
177
+ }
178
+ }
179
+
180
+ // If any nested child is open, abort.
181
+ if (hasOpenChildNode()) {
182
+ return undefined;
183
+ }
184
+
185
+ const refRect = domReference.getBoundingClientRect();
186
+ const rect = floating.getBoundingClientRect();
187
+ const cursorLeaveFromRight = x > rect.right - rect.width / 2;
188
+ const cursorLeaveFromBottom = y > rect.bottom - rect.height / 2;
189
+ const isFloatingWider = rect.width > refRect.width;
190
+ const isFloatingTaller = rect.height > refRect.height;
191
+ const left = (isFloatingWider ? refRect : rect).left;
192
+ const right = (isFloatingWider ? refRect : rect).right;
193
+ const top = (isFloatingTaller ? refRect : rect).top;
194
+ const bottom = (isFloatingTaller ? refRect : rect).bottom;
195
+
196
+ // If the pointer is leaving from the opposite side, the "buffer" logic creates a point
197
+ // where the floating element remains open, but should be ignored. A constant of 1
198
+ // handles floating point rounding errors.
199
+ if (
200
+ (side === 'top' && y >= refRect.bottom - 1) ||
201
+ (side === 'bottom' && y <= refRect.top + 1) ||
202
+ (side === 'left' && x >= refRect.right - 1) ||
203
+ (side === 'right' && x <= refRect.left + 1)
204
+ ) {
205
+ closeIfNoOpenChild();
206
+ return undefined;
207
+ }
208
+
209
+ // Ignore when the cursor is within the rectangular trough between the two elements.
210
+ // Since the triangle is created from the cursor point, which can start beyond the ref
211
+ // element's edge, traversing back and forth from the ref to the floating element can
212
+ // cause it to close. This ensures it always remains open in that case.
213
+ let isInsideTroughRect = false;
214
+
215
+ switch (side) {
216
+ case 'top':
217
+ isInsideTroughRect = isInsideAxisAlignedRect(
218
+ clientX,
219
+ clientY,
220
+ left,
221
+ refRect.top + 1,
222
+ right,
223
+ rect.bottom - 1,
224
+ );
225
+ break;
226
+ case 'bottom':
227
+ isInsideTroughRect = isInsideAxisAlignedRect(
228
+ clientX,
229
+ clientY,
230
+ left,
231
+ rect.top + 1,
232
+ right,
233
+ refRect.bottom - 1,
234
+ );
235
+ break;
236
+ case 'left':
237
+ isInsideTroughRect = isInsideAxisAlignedRect(
238
+ clientX,
239
+ clientY,
240
+ rect.right - 1,
241
+ bottom,
242
+ refRect.left + 1,
243
+ top,
244
+ );
245
+ break;
246
+ case 'right':
247
+ isInsideTroughRect = isInsideAxisAlignedRect(
248
+ clientX,
249
+ clientY,
250
+ refRect.right - 1,
251
+ bottom,
252
+ rect.left + 1,
253
+ top,
254
+ );
255
+ break;
256
+ default:
257
+ }
258
+
259
+ if (isInsideTroughRect) {
260
+ return undefined;
261
+ }
262
+
263
+ if (hasLanded && !isInsideRect(clientX, clientY, refRect)) {
264
+ closeIfNoOpenChild();
265
+ return undefined;
266
+ }
267
+
268
+ if (!isLeave && isCursorMovingSlowly(clientX, clientY)) {
269
+ closeIfNoOpenChild();
270
+ return undefined;
271
+ }
272
+
273
+ let isInsidePolygon = false;
274
+
275
+ switch (side) {
276
+ case 'top': {
277
+ const cursorXOffset = isFloatingWider ? POLYGON_BUFFER / 2 : POLYGON_BUFFER * 4;
278
+ const cursorPointOneX = isFloatingWider
279
+ ? x + cursorXOffset
280
+ : cursorLeaveFromRight
281
+ ? x + cursorXOffset
282
+ : x - cursorXOffset;
283
+ const cursorPointTwoX = isFloatingWider
284
+ ? x - cursorXOffset
285
+ : cursorLeaveFromRight
286
+ ? x + cursorXOffset
287
+ : x - cursorXOffset;
288
+ const cursorPointY = y + POLYGON_BUFFER + 1;
289
+
290
+ const commonYLeft = cursorLeaveFromRight
291
+ ? rect.bottom - POLYGON_BUFFER
292
+ : isFloatingWider
293
+ ? rect.bottom - POLYGON_BUFFER
294
+ : rect.top;
295
+ const commonYRight = cursorLeaveFromRight
296
+ ? isFloatingWider
297
+ ? rect.bottom - POLYGON_BUFFER
298
+ : rect.top
299
+ : rect.bottom - POLYGON_BUFFER;
300
+
301
+ isInsidePolygon = isPointInQuadrilateral(
302
+ clientX,
303
+ clientY,
304
+ cursorPointOneX,
305
+ cursorPointY,
306
+ cursorPointTwoX,
307
+ cursorPointY,
308
+ rect.left,
309
+ commonYLeft,
310
+ rect.right,
311
+ commonYRight,
312
+ );
313
+ break;
314
+ }
315
+ case 'bottom': {
316
+ const cursorXOffset = isFloatingWider ? POLYGON_BUFFER / 2 : POLYGON_BUFFER * 4;
317
+ const cursorPointOneX = isFloatingWider
318
+ ? x + cursorXOffset
319
+ : cursorLeaveFromRight
320
+ ? x + cursorXOffset
321
+ : x - cursorXOffset;
322
+ const cursorPointTwoX = isFloatingWider
323
+ ? x - cursorXOffset
324
+ : cursorLeaveFromRight
325
+ ? x + cursorXOffset
326
+ : x - cursorXOffset;
327
+ const cursorPointY = y - POLYGON_BUFFER;
328
+
329
+ const commonYLeft = cursorLeaveFromRight
330
+ ? rect.top + POLYGON_BUFFER
331
+ : isFloatingWider
332
+ ? rect.top + POLYGON_BUFFER
333
+ : rect.bottom;
334
+ const commonYRight = cursorLeaveFromRight
335
+ ? isFloatingWider
336
+ ? rect.top + POLYGON_BUFFER
337
+ : rect.bottom
338
+ : rect.top + POLYGON_BUFFER;
339
+
340
+ isInsidePolygon = isPointInQuadrilateral(
341
+ clientX,
342
+ clientY,
343
+ cursorPointOneX,
344
+ cursorPointY,
345
+ cursorPointTwoX,
346
+ cursorPointY,
347
+ rect.left,
348
+ commonYLeft,
349
+ rect.right,
350
+ commonYRight,
351
+ );
352
+ break;
353
+ }
354
+ case 'left': {
355
+ const cursorYOffset = isFloatingTaller ? POLYGON_BUFFER / 2 : POLYGON_BUFFER * 4;
356
+ const cursorPointOneY = isFloatingTaller
357
+ ? y + cursorYOffset
358
+ : cursorLeaveFromBottom
359
+ ? y + cursorYOffset
360
+ : y - cursorYOffset;
361
+ const cursorPointTwoY = isFloatingTaller
362
+ ? y - cursorYOffset
363
+ : cursorLeaveFromBottom
364
+ ? y + cursorYOffset
365
+ : y - cursorYOffset;
366
+ const cursorPointX = x + POLYGON_BUFFER + 1;
367
+
368
+ const commonXTop = cursorLeaveFromBottom
369
+ ? rect.right - POLYGON_BUFFER
370
+ : isFloatingTaller
371
+ ? rect.right - POLYGON_BUFFER
372
+ : rect.left;
373
+ const commonXBottom = cursorLeaveFromBottom
374
+ ? isFloatingTaller
375
+ ? rect.right - POLYGON_BUFFER
376
+ : rect.left
377
+ : rect.right - POLYGON_BUFFER;
378
+
379
+ isInsidePolygon = isPointInQuadrilateral(
380
+ clientX,
381
+ clientY,
382
+ commonXTop,
383
+ rect.top,
384
+ commonXBottom,
385
+ rect.bottom,
386
+ cursorPointX,
387
+ cursorPointOneY,
388
+ cursorPointX,
389
+ cursorPointTwoY,
390
+ );
391
+ break;
392
+ }
393
+ case 'right': {
394
+ const cursorYOffset = isFloatingTaller ? POLYGON_BUFFER / 2 : POLYGON_BUFFER * 4;
395
+ const cursorPointOneY = isFloatingTaller
396
+ ? y + cursorYOffset
397
+ : cursorLeaveFromBottom
398
+ ? y + cursorYOffset
399
+ : y - cursorYOffset;
400
+ const cursorPointTwoY = isFloatingTaller
401
+ ? y - cursorYOffset
402
+ : cursorLeaveFromBottom
403
+ ? y + cursorYOffset
404
+ : y - cursorYOffset;
405
+ const cursorPointX = x - POLYGON_BUFFER;
406
+
407
+ const commonXTop = cursorLeaveFromBottom
408
+ ? rect.left + POLYGON_BUFFER
409
+ : isFloatingTaller
410
+ ? rect.left + POLYGON_BUFFER
411
+ : rect.right;
412
+ const commonXBottom = cursorLeaveFromBottom
413
+ ? isFloatingTaller
414
+ ? rect.left + POLYGON_BUFFER
415
+ : rect.right
416
+ : rect.left + POLYGON_BUFFER;
417
+
418
+ isInsidePolygon = isPointInQuadrilateral(
419
+ clientX,
420
+ clientY,
421
+ cursorPointX,
422
+ cursorPointOneY,
423
+ cursorPointX,
424
+ cursorPointTwoY,
425
+ commonXTop,
426
+ rect.top,
427
+ commonXBottom,
428
+ rect.bottom,
429
+ );
430
+ break;
431
+ }
432
+ default:
433
+ }
434
+
435
+ if (!isInsidePolygon) {
436
+ closeIfNoOpenChild();
437
+ } else if (!hasLanded) {
438
+ timeout.start(40, closeIfNoOpenChild);
439
+ }
440
+
441
+ return undefined;
442
+ };
443
+ };
444
+
445
+ fn.__options = { ...options, blockPointerEvents };
446
+
447
+ return fn;
448
+ }
@@ -0,0 +1,276 @@
1
+ // Ported from .base-ui/packages/react/src/floating-ui-react/hooks/useClientPoint.ts (v1.6.0).
2
+ // Positions the popup relative to a client point (the cursor) instead of the trigger's box, by
3
+ // installing a virtual reference element whose rect follows the pointer on the chosen axis.
4
+ //
5
+ // octane adaptations: handlers receive the NATIVE event; every composed hook threads an explicit
6
+ // slot.
7
+ //
8
+ // SLOT: plain-`.ts` hook; the trailing arg is the caller's slot.
9
+ import { useEffect, useMemo, useRef, useState } from 'octane';
10
+
11
+ import { S, splitSlot, subSlot } from '../../internal';
12
+ import { addEventListener } from '../addEventListener';
13
+ import { ownerWindow } from '../owner';
14
+ import { useStableCallback } from '../useStableCallback';
15
+ import { contains, getTarget } from './element';
16
+ import { isMouseLikePointerType } from './event';
17
+
18
+ function createVirtualElement(
19
+ domElement: Element | null | undefined,
20
+ data: {
21
+ axis: 'x' | 'y' | 'both';
22
+ dataRef: { current: any };
23
+ pointerType: string | undefined;
24
+ x: number | null;
25
+ y: number | null;
26
+ },
27
+ ): any {
28
+ let offsetX: number | null = null;
29
+ let offsetY: number | null = null;
30
+ let isAutoUpdateEvent = false;
31
+
32
+ return {
33
+ contextElement: domElement || undefined,
34
+ getBoundingClientRect() {
35
+ const domRect = domElement?.getBoundingClientRect() || { width: 0, height: 0, x: 0, y: 0 };
36
+
37
+ const isXAxis = data.axis === 'x' || data.axis === 'both';
38
+ const isYAxis = data.axis === 'y' || data.axis === 'both';
39
+ const canTrackCursorOnAutoUpdate =
40
+ ['mouseenter', 'mousemove'].includes(data.dataRef.current.openEvent?.type || '') &&
41
+ data.pointerType !== 'touch';
42
+
43
+ let width = domRect.width;
44
+ let height = domRect.height;
45
+ let x = domRect.x;
46
+ let y = domRect.y;
47
+
48
+ if (offsetX == null && data.x && isXAxis) {
49
+ offsetX = domRect.x - data.x;
50
+ }
51
+
52
+ if (offsetY == null && data.y && isYAxis) {
53
+ offsetY = domRect.y - data.y;
54
+ }
55
+
56
+ x -= offsetX || 0;
57
+ y -= offsetY || 0;
58
+ width = 0;
59
+ height = 0;
60
+
61
+ if (!isAutoUpdateEvent || canTrackCursorOnAutoUpdate) {
62
+ width = data.axis === 'y' ? domRect.width : 0;
63
+ height = data.axis === 'x' ? domRect.height : 0;
64
+ x = isXAxis && data.x != null ? data.x : x;
65
+ y = isYAxis && data.y != null ? data.y : y;
66
+ } else if (isAutoUpdateEvent && !canTrackCursorOnAutoUpdate) {
67
+ height = data.axis === 'x' ? domRect.height : height;
68
+ width = data.axis === 'y' ? domRect.width : width;
69
+ }
70
+
71
+ isAutoUpdateEvent = true;
72
+
73
+ return { width, height, x, y, top: y, right: x + width, bottom: y + height, left: x };
74
+ },
75
+ };
76
+ }
77
+
78
+ function isMouseBasedEvent(event: Event | undefined): event is MouseEvent {
79
+ return event != null && (event as MouseEvent).clientX != null;
80
+ }
81
+
82
+ export interface UseClientPointProps {
83
+ /**
84
+ * Whether the hook is enabled, including all internal effects and event handlers.
85
+ * @default true
86
+ */
87
+ enabled?: boolean | undefined;
88
+ /**
89
+ * Whether to restrict the client point to an axis and use the reference element (if it exists)
90
+ * as the other axis. Useful when the floating element is also interactive.
91
+ * @default 'both'
92
+ */
93
+ axis?: 'x' | 'y' | 'both' | undefined;
94
+ }
95
+
96
+ export function useClientPoint(...args: any[]): Record<string, any> {
97
+ const [user, slotArg] = splitSlot(args);
98
+ const slot = slotArg ?? S('useClientPoint');
99
+ const context = user[0] as any;
100
+ const props = (user[1] as UseClientPointProps) ?? {};
101
+
102
+ const { enabled = true, axis = 'both' } = props;
103
+
104
+ const store = context != null && 'rootStore' in context ? context.rootStore : context;
105
+
106
+ const open = store.useState('open', subSlot(slot, 's:open'));
107
+ const floating = store.useState('floatingElement', subSlot(slot, 's:floating'));
108
+ const domReference = store.useState('domReferenceElement', subSlot(slot, 's:ref'));
109
+
110
+ const dataRef = store.context.dataRef;
111
+
112
+ const initialRef = useRef(false, subSlot(slot, 'initial'));
113
+ const cleanupListenerRef = useRef<null | (() => void)>(null, subSlot(slot, 'cleanup'));
114
+
115
+ const [pointerType, setPointerType] = useState<string | undefined>(
116
+ undefined,
117
+ subSlot(slot, 'ptype'),
118
+ );
119
+ const [reactive, setReactive] = useState<never[]>([], subSlot(slot, 'reactive'));
120
+
121
+ const resetReference = useStableCallback(
122
+ (reference: Element | null) => {
123
+ store.set('positionReference', reference);
124
+ },
125
+ subSlot(slot, 'reset'),
126
+ );
127
+
128
+ const setReference = useStableCallback(
129
+ (newX: number | null, newY: number | null, referenceElement?: Element | null) => {
130
+ if (initialRef.current) {
131
+ return;
132
+ }
133
+
134
+ // Don't set if the open event wasn't mouse-like (e.g. focus to open, then hover the
135
+ // reference). Only applies when the event exists.
136
+ if (dataRef.current.openEvent && !isMouseBasedEvent(dataRef.current.openEvent)) {
137
+ return;
138
+ }
139
+
140
+ store.set(
141
+ 'positionReference',
142
+ createVirtualElement(referenceElement ?? domReference, {
143
+ x: newX,
144
+ y: newY,
145
+ axis,
146
+ dataRef,
147
+ pointerType,
148
+ }),
149
+ );
150
+ },
151
+ subSlot(slot, 'setRef'),
152
+ );
153
+
154
+ const handleReferenceEnterOrMove = useStableCallback(
155
+ (event: MouseEvent) => {
156
+ if (!open) {
157
+ setReference(event.clientX, event.clientY, event.currentTarget as Element);
158
+ } else if (!cleanupListenerRef.current) {
159
+ // With no cleanup there's no listener; re-add it for the case where the cursor landed on
160
+ // the floating element and then came back to the reference (an interactive popup).
161
+ setReference(event.clientX, event.clientY, event.currentTarget as Element);
162
+ setReactive([]);
163
+ }
164
+ },
165
+ subSlot(slot, 'enterMove'),
166
+ );
167
+
168
+ // For a mouse-like pointer keep following the cursor even while the popup transitions out. On
169
+ // touch that's undesirable — the popup would jump to the dismissal touch point.
170
+ const openCheck = isMouseLikePointerType(pointerType) ? floating : open;
171
+
172
+ useEffect(
173
+ () => {
174
+ if (!enabled) {
175
+ resetReference(domReference);
176
+ return undefined;
177
+ }
178
+
179
+ if (!openCheck) {
180
+ return undefined;
181
+ }
182
+
183
+ function cleanupListener() {
184
+ cleanupListenerRef.current?.();
185
+ cleanupListenerRef.current = null;
186
+ }
187
+
188
+ const win = ownerWindow(floating);
189
+
190
+ function handleMouseMove(event: MouseEvent) {
191
+ const target = getTarget(event) as Element | null;
192
+ if (!contains(floating, target)) {
193
+ setReference(event.clientX, event.clientY);
194
+ } else {
195
+ cleanupListener();
196
+ }
197
+ }
198
+
199
+ if (!dataRef.current.openEvent || isMouseBasedEvent(dataRef.current.openEvent)) {
200
+ cleanupListenerRef.current = addEventListener(
201
+ win,
202
+ 'mousemove',
203
+ handleMouseMove as EventListener,
204
+ );
205
+ } else {
206
+ resetReference(domReference);
207
+ }
208
+
209
+ return cleanupListener;
210
+ },
211
+ [
212
+ openCheck,
213
+ enabled,
214
+ floating,
215
+ dataRef,
216
+ domReference,
217
+ store,
218
+ setReference,
219
+ resetReference,
220
+ reactive,
221
+ ],
222
+ subSlot(slot, 'e:track'),
223
+ );
224
+
225
+ // Clear virtual cursor references when the hook unmounts. Enabled flips are handled above.
226
+ useEffect(
227
+ () => () => {
228
+ store.set('positionReference', null);
229
+ },
230
+ [store],
231
+ subSlot(slot, 'e:unmount'),
232
+ );
233
+
234
+ useEffect(
235
+ () => {
236
+ if (enabled && !floating) {
237
+ initialRef.current = false;
238
+ }
239
+ },
240
+ [enabled, floating],
241
+ subSlot(slot, 'e:reset'),
242
+ );
243
+
244
+ useEffect(
245
+ () => {
246
+ if (!enabled && open) {
247
+ initialRef.current = true;
248
+ }
249
+ },
250
+ [enabled, open],
251
+ subSlot(slot, 'e:freeze'),
252
+ );
253
+
254
+ const reference = useMemo(
255
+ () => {
256
+ function setPointerTypeRef(event: PointerEvent) {
257
+ setPointerType(event.pointerType);
258
+ }
259
+
260
+ return {
261
+ onPointerDown: setPointerTypeRef,
262
+ onPointerEnter: setPointerTypeRef,
263
+ onMouseMove: handleReferenceEnterOrMove,
264
+ onMouseEnter: handleReferenceEnterOrMove,
265
+ };
266
+ },
267
+ [handleReferenceEnterOrMove],
268
+ subSlot(slot, 'ref'),
269
+ );
270
+
271
+ return useMemo(
272
+ () => (enabled ? { reference, trigger: reference } : {}),
273
+ [enabled, reference],
274
+ subSlot(slot, 'out'),
275
+ );
276
+ }