@tamagui/floating 2.0.0-rc.3 → 2.0.0-rc.30

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 (190) hide show
  1. package/dist/cjs/Floating.native.js +4 -0
  2. package/dist/cjs/Floating.native.js.map +1 -1
  3. package/dist/cjs/index.cjs +33 -2
  4. package/dist/cjs/index.js +49 -12
  5. package/dist/cjs/index.js.map +2 -2
  6. package/dist/cjs/index.native.js +33 -2
  7. package/dist/cjs/index.native.js.map +1 -1
  8. package/dist/cjs/interactions/PopupTriggerMap.cjs +40 -0
  9. package/dist/cjs/interactions/PopupTriggerMap.native.js +73 -0
  10. package/dist/cjs/interactions/PopupTriggerMap.native.js.map +1 -0
  11. package/dist/cjs/interactions/createFloatingEvents.cjs +41 -0
  12. package/dist/cjs/interactions/createFloatingEvents.native.js +47 -0
  13. package/dist/cjs/interactions/createFloatingEvents.native.js.map +1 -0
  14. package/dist/cjs/interactions/safePolygon.cjs +191 -0
  15. package/dist/cjs/interactions/safePolygon.native.js +205 -0
  16. package/dist/cjs/interactions/safePolygon.native.js.map +1 -0
  17. package/dist/cjs/interactions/types.cjs +16 -0
  18. package/dist/cjs/interactions/types.native.js +19 -0
  19. package/dist/cjs/interactions/types.native.js.map +1 -0
  20. package/dist/cjs/interactions/useClick.cjs +82 -0
  21. package/dist/cjs/interactions/useClick.native.js +90 -0
  22. package/dist/cjs/interactions/useClick.native.js.map +1 -0
  23. package/dist/cjs/interactions/useDelayGroup.cjs +96 -0
  24. package/dist/cjs/interactions/useDelayGroup.native.js +109 -0
  25. package/dist/cjs/interactions/useDelayGroup.native.js.map +1 -0
  26. package/dist/cjs/interactions/useFocus.cjs +101 -0
  27. package/dist/cjs/interactions/useFocus.native.js +113 -0
  28. package/dist/cjs/interactions/useFocus.native.js.map +1 -0
  29. package/dist/cjs/interactions/useHover.cjs +224 -0
  30. package/dist/cjs/interactions/useHover.native.js +248 -0
  31. package/dist/cjs/interactions/useHover.native.js.map +1 -0
  32. package/dist/cjs/interactions/useInnerOffset.cjs +104 -0
  33. package/dist/cjs/interactions/useInnerOffset.native.js +118 -0
  34. package/dist/cjs/interactions/useInnerOffset.native.js.map +1 -0
  35. package/dist/cjs/interactions/useInteractions.cjs +76 -0
  36. package/dist/cjs/interactions/useInteractions.native.js +164 -0
  37. package/dist/cjs/interactions/useInteractions.native.js.map +1 -0
  38. package/dist/cjs/interactions/useListNavigation.cjs +253 -0
  39. package/dist/cjs/interactions/useListNavigation.native.js +297 -0
  40. package/dist/cjs/interactions/useListNavigation.native.js.map +1 -0
  41. package/dist/cjs/interactions/useRole.cjs +112 -0
  42. package/dist/cjs/interactions/useRole.native.js +128 -0
  43. package/dist/cjs/interactions/useRole.native.js.map +1 -0
  44. package/dist/cjs/interactions/useTypeahead.cjs +93 -0
  45. package/dist/cjs/interactions/useTypeahead.native.js +114 -0
  46. package/dist/cjs/interactions/useTypeahead.native.js.map +1 -0
  47. package/dist/cjs/interactions/utils.cjs +178 -0
  48. package/dist/cjs/interactions/utils.native.js +188 -0
  49. package/dist/cjs/interactions/utils.native.js.map +1 -0
  50. package/dist/cjs/middleware/inner.cjs +106 -0
  51. package/dist/cjs/middleware/inner.native.js +120 -0
  52. package/dist/cjs/middleware/inner.native.js.map +1 -0
  53. package/dist/cjs/useFloating.cjs +21 -17
  54. package/dist/cjs/useFloating.native.js +2 -0
  55. package/dist/cjs/useFloating.native.js.map +1 -1
  56. package/dist/esm/Floating.native.js +4 -1
  57. package/dist/esm/Floating.native.js.map +1 -1
  58. package/dist/esm/index.js +17 -34
  59. package/dist/esm/index.js.map +1 -6
  60. package/dist/esm/index.mjs +16 -2
  61. package/dist/esm/index.mjs.map +1 -1
  62. package/dist/esm/index.native.js +16 -2
  63. package/dist/esm/index.native.js.map +1 -1
  64. package/dist/esm/interactions/PopupTriggerMap.mjs +17 -0
  65. package/dist/esm/interactions/PopupTriggerMap.mjs.map +1 -0
  66. package/dist/esm/interactions/PopupTriggerMap.native.js +47 -0
  67. package/dist/esm/interactions/PopupTriggerMap.native.js.map +1 -0
  68. package/dist/esm/interactions/createFloatingEvents.mjs +18 -0
  69. package/dist/esm/interactions/createFloatingEvents.mjs.map +1 -0
  70. package/dist/esm/interactions/createFloatingEvents.native.js +21 -0
  71. package/dist/esm/interactions/createFloatingEvents.native.js.map +1 -0
  72. package/dist/esm/interactions/safePolygon.mjs +168 -0
  73. package/dist/esm/interactions/safePolygon.mjs.map +1 -0
  74. package/dist/esm/interactions/safePolygon.native.js +179 -0
  75. package/dist/esm/interactions/safePolygon.native.js.map +1 -0
  76. package/dist/esm/interactions/types.mjs +2 -0
  77. package/dist/esm/interactions/types.mjs.map +1 -0
  78. package/dist/esm/interactions/types.native.js +2 -0
  79. package/dist/esm/interactions/types.native.js.map +1 -0
  80. package/dist/esm/interactions/useClick.mjs +59 -0
  81. package/dist/esm/interactions/useClick.mjs.map +1 -0
  82. package/dist/esm/interactions/useClick.native.js +64 -0
  83. package/dist/esm/interactions/useClick.native.js.map +1 -0
  84. package/dist/esm/interactions/useDelayGroup.mjs +60 -0
  85. package/dist/esm/interactions/useDelayGroup.mjs.map +1 -0
  86. package/dist/esm/interactions/useDelayGroup.native.js +70 -0
  87. package/dist/esm/interactions/useDelayGroup.native.js.map +1 -0
  88. package/dist/esm/interactions/useFocus.mjs +78 -0
  89. package/dist/esm/interactions/useFocus.mjs.map +1 -0
  90. package/dist/esm/interactions/useFocus.native.js +87 -0
  91. package/dist/esm/interactions/useFocus.native.js.map +1 -0
  92. package/dist/esm/interactions/useHover.mjs +189 -0
  93. package/dist/esm/interactions/useHover.mjs.map +1 -0
  94. package/dist/esm/interactions/useHover.native.js +210 -0
  95. package/dist/esm/interactions/useHover.native.js.map +1 -0
  96. package/dist/esm/interactions/useInnerOffset.mjs +70 -0
  97. package/dist/esm/interactions/useInnerOffset.mjs.map +1 -0
  98. package/dist/esm/interactions/useInnerOffset.native.js +81 -0
  99. package/dist/esm/interactions/useInnerOffset.native.js.map +1 -0
  100. package/dist/esm/interactions/useInteractions.mjs +53 -0
  101. package/dist/esm/interactions/useInteractions.mjs.map +1 -0
  102. package/dist/esm/interactions/useInteractions.native.js +138 -0
  103. package/dist/esm/interactions/useInteractions.native.js.map +1 -0
  104. package/dist/esm/interactions/useListNavigation.mjs +230 -0
  105. package/dist/esm/interactions/useListNavigation.mjs.map +1 -0
  106. package/dist/esm/interactions/useListNavigation.native.js +271 -0
  107. package/dist/esm/interactions/useListNavigation.native.js.map +1 -0
  108. package/dist/esm/interactions/useRole.mjs +78 -0
  109. package/dist/esm/interactions/useRole.mjs.map +1 -0
  110. package/dist/esm/interactions/useRole.native.js +91 -0
  111. package/dist/esm/interactions/useRole.native.js.map +1 -0
  112. package/dist/esm/interactions/useTypeahead.mjs +70 -0
  113. package/dist/esm/interactions/useTypeahead.mjs.map +1 -0
  114. package/dist/esm/interactions/useTypeahead.native.js +88 -0
  115. package/dist/esm/interactions/useTypeahead.native.js.map +1 -0
  116. package/dist/esm/interactions/utils.mjs +134 -0
  117. package/dist/esm/interactions/utils.mjs.map +1 -0
  118. package/dist/esm/interactions/utils.native.js +141 -0
  119. package/dist/esm/interactions/utils.native.js.map +1 -0
  120. package/dist/esm/middleware/inner.mjs +72 -0
  121. package/dist/esm/middleware/inner.mjs.map +1 -0
  122. package/dist/esm/middleware/inner.native.js +83 -0
  123. package/dist/esm/middleware/inner.native.js.map +1 -0
  124. package/dist/esm/useFloating.mjs +21 -17
  125. package/dist/esm/useFloating.mjs.map +1 -1
  126. package/dist/esm/useFloating.native.js +2 -0
  127. package/dist/esm/useFloating.native.js.map +1 -1
  128. package/package.json +8 -10
  129. package/src/Floating.native.tsx +1 -0
  130. package/src/index.ts +49 -0
  131. package/src/interactions/PopupTriggerMap.ts +30 -0
  132. package/src/interactions/createFloatingEvents.ts +34 -0
  133. package/src/interactions/safePolygon.ts +500 -0
  134. package/src/interactions/types.ts +165 -0
  135. package/src/interactions/useClick.ts +148 -0
  136. package/src/interactions/useDelayGroup.ts +114 -0
  137. package/src/interactions/useFocus.ts +164 -0
  138. package/src/interactions/useHover.ts +453 -0
  139. package/src/interactions/useInnerOffset.ts +116 -0
  140. package/src/interactions/useInteractions.ts +101 -0
  141. package/src/interactions/useListNavigation.ts +578 -0
  142. package/src/interactions/useRole.ts +103 -0
  143. package/src/interactions/useTypeahead.ts +173 -0
  144. package/src/interactions/utils.ts +234 -0
  145. package/src/middleware/inner.ts +142 -0
  146. package/src/useFloating.tsx +13 -1
  147. package/types/Floating.native.d.ts +1 -0
  148. package/types/Floating.native.d.ts.map +1 -1
  149. package/types/index.d.ts +17 -2
  150. package/types/index.d.ts.map +1 -1
  151. package/types/interactions/PopupTriggerMap.d.ts +8 -0
  152. package/types/interactions/PopupTriggerMap.d.ts.map +1 -0
  153. package/types/interactions/createFloatingEvents.d.ts +7 -0
  154. package/types/interactions/createFloatingEvents.d.ts.map +1 -0
  155. package/types/interactions/safePolygon.d.ts +4 -0
  156. package/types/interactions/safePolygon.d.ts.map +1 -0
  157. package/types/interactions/types.d.ts +123 -0
  158. package/types/interactions/types.d.ts.map +1 -0
  159. package/types/interactions/useClick.d.ts +3 -0
  160. package/types/interactions/useClick.d.ts.map +1 -0
  161. package/types/interactions/useDelayGroup.d.ts +23 -0
  162. package/types/interactions/useDelayGroup.d.ts.map +1 -0
  163. package/types/interactions/useFocus.d.ts +3 -0
  164. package/types/interactions/useFocus.d.ts.map +1 -0
  165. package/types/interactions/useHover.d.ts +6 -0
  166. package/types/interactions/useHover.d.ts.map +1 -0
  167. package/types/interactions/useInnerOffset.d.ts +3 -0
  168. package/types/interactions/useInnerOffset.d.ts.map +1 -0
  169. package/types/interactions/useInteractions.d.ts +8 -0
  170. package/types/interactions/useInteractions.d.ts.map +1 -0
  171. package/types/interactions/useListNavigation.d.ts +3 -0
  172. package/types/interactions/useListNavigation.d.ts.map +1 -0
  173. package/types/interactions/useRole.d.ts +3 -0
  174. package/types/interactions/useRole.d.ts.map +1 -0
  175. package/types/interactions/useTypeahead.d.ts +3 -0
  176. package/types/interactions/useTypeahead.d.ts.map +1 -0
  177. package/types/interactions/utils.d.ts +46 -0
  178. package/types/interactions/utils.d.ts.map +1 -0
  179. package/types/middleware/inner.d.ts +14 -0
  180. package/types/middleware/inner.d.ts.map +1 -0
  181. package/types/useFloating.d.ts +7 -1
  182. package/types/useFloating.d.ts.map +1 -1
  183. package/dist/cjs/Floating.js +0 -15
  184. package/dist/cjs/Floating.js.map +0 -6
  185. package/dist/cjs/useFloating.js +0 -46
  186. package/dist/cjs/useFloating.js.map +0 -6
  187. package/dist/esm/Floating.js +0 -2
  188. package/dist/esm/Floating.js.map +0 -6
  189. package/dist/esm/useFloating.js +0 -23
  190. package/dist/esm/useFloating.js.map +0 -6
package/src/index.ts CHANGED
@@ -49,6 +49,7 @@ export {
49
49
  autoUpdate,
50
50
  detectOverflow,
51
51
  flip,
52
+ getOverflowAncestors,
52
53
  hide,
53
54
  inline,
54
55
  limitShift,
@@ -64,4 +65,52 @@ export {
64
65
  type UseFloatingReturn,
65
66
  type UseFloatingProps,
66
67
  type UseFloatingFn,
68
+ type UseFloatingOverrideFn,
67
69
  } from './useFloating'
70
+
71
+ // raw useFloating without FloatingOverrideContext — use when building
72
+ // override context factories to avoid infinite recursion
73
+ export { useFloating as useFloatingRaw } from './Floating'
74
+
75
+ // event emitter for hook coordination
76
+ export { createFloatingEvents } from './interactions/createFloatingEvents'
77
+
78
+ // multi-trigger coordination
79
+ export { PopupTriggerMap } from './interactions/PopupTriggerMap'
80
+
81
+ // interaction hooks
82
+ export { useInteractions } from './interactions/useInteractions'
83
+ export { useHover } from './interactions/useHover'
84
+ export { safePolygon } from './interactions/safePolygon'
85
+ export { useFocus } from './interactions/useFocus'
86
+ export { useRole } from './interactions/useRole'
87
+ export { useClick } from './interactions/useClick'
88
+ export { useListNavigation } from './interactions/useListNavigation'
89
+ export { useTypeahead } from './interactions/useTypeahead'
90
+ export { useInnerOffset } from './interactions/useInnerOffset'
91
+ export {
92
+ FloatingDelayGroup,
93
+ useDelayGroup,
94
+ useDelayGroupContext,
95
+ } from './interactions/useDelayGroup'
96
+
97
+ // middleware
98
+ export { inner } from './middleware/inner'
99
+
100
+ // types
101
+ export type {
102
+ ElementProps,
103
+ FloatingEvents,
104
+ FloatingInteractionContext,
105
+ OpenChangeReason,
106
+ UseHoverProps,
107
+ HandleCloseFn,
108
+ SafePolygonOptions,
109
+ UseFocusProps,
110
+ UseRoleProps,
111
+ UseClickProps,
112
+ UseListNavigationProps,
113
+ UseTypeaheadProps,
114
+ UseInnerOffsetProps,
115
+ Delay,
116
+ } from './interactions/types'
@@ -0,0 +1,30 @@
1
+ // tracks trigger elements for multi-trigger tooltip/popover patterns.
2
+ // when multiple triggers share a single floating element (scoped pattern),
3
+ // this lets useHover check if the cursor moved to a sibling trigger
4
+ // and suppress the close.
5
+
6
+ export class PopupTriggerMap {
7
+ private map = new Map<string, Element>()
8
+ private elements = new Set<Element>()
9
+
10
+ add(id: string, element: Element) {
11
+ const prev = this.map.get(id)
12
+ if (prev) {
13
+ this.elements.delete(prev)
14
+ }
15
+ this.map.set(id, element)
16
+ this.elements.add(element)
17
+ }
18
+
19
+ delete(id: string) {
20
+ const el = this.map.get(id)
21
+ if (el) {
22
+ this.elements.delete(el)
23
+ this.map.delete(id)
24
+ }
25
+ }
26
+
27
+ hasElement(element: Element): boolean {
28
+ return this.elements.has(element)
29
+ }
30
+ }
@@ -0,0 +1,34 @@
1
+ // lightweight event emitter for coordinating between interaction hooks.
2
+ // when one hook changes open state (e.g. dismiss via escape), others
3
+ // need to know so they can clear their timers and prevent reopening.
4
+
5
+ export type FloatingEvents = {
6
+ emit(event: string, data?: any): void
7
+ on(event: string, handler: (data?: any) => void): void
8
+ off(event: string, handler: (data?: any) => void): void
9
+ }
10
+
11
+ export function createFloatingEvents(): FloatingEvents {
12
+ const listeners = new Map<string, Set<(data?: any) => void>>()
13
+
14
+ return {
15
+ emit(event, data) {
16
+ listeners.get(event)?.forEach((fn) => fn(data))
17
+ },
18
+ on(event, handler) {
19
+ let set = listeners.get(event)
20
+ if (!set) {
21
+ set = new Set()
22
+ listeners.set(event, set)
23
+ }
24
+ set.add(handler)
25
+ },
26
+ off(event, handler) {
27
+ const set = listeners.get(event)
28
+ if (set) {
29
+ set.delete(handler)
30
+ if (set.size === 0) listeners.delete(event)
31
+ }
32
+ },
33
+ }
34
+ }
@@ -0,0 +1,500 @@
1
+ import type { HandleCloseFn, SafePolygonOptions } from './types'
2
+ import { clearTimeoutIfSet, contains, getTarget } from './utils'
3
+
4
+ type Point = [number, number]
5
+ type Polygon = Point[]
6
+ type Side = 'top' | 'bottom' | 'left' | 'right'
7
+ type Rect = { x: number; y: number; width: number; height: number }
8
+
9
+ function isPointInPolygon(point: Point, polygon: Polygon) {
10
+ const [x, y] = point
11
+ let isInside = false
12
+ const length = polygon.length
13
+ for (let i = 0, j = length - 1; i < length; j = i++) {
14
+ const [xi, yi] = polygon[i] || [0, 0]
15
+ const [xj, yj] = polygon[j] || [0, 0]
16
+ const intersect = yi >= y !== yj >= y && x <= ((xj - xi) * (y - yi)) / (yj - yi) + xi
17
+ if (intersect) {
18
+ isInside = !isInside
19
+ }
20
+ }
21
+ return isInside
22
+ }
23
+
24
+ function isInside(point: Point, rect: Rect) {
25
+ return (
26
+ point[0] >= rect.x &&
27
+ point[0] <= rect.x + rect.width &&
28
+ point[1] >= rect.y &&
29
+ point[1] <= rect.y + rect.height
30
+ )
31
+ }
32
+
33
+ export type { SafePolygonOptions }
34
+
35
+ // generates a safe polygon area that the user can traverse without closing the
36
+ // floating element once leaving the reference element.
37
+ // ported from @floating-ui/react with full polygon geometry.
38
+ //
39
+ // the returned HandleCloseFn is a closure factory: called once on mouseleave
40
+ // with the leave position (x, y), it returns a handler that runs on each
41
+ // subsequent document mousemove. the original leave position is baked into
42
+ // the closure so the polygon anchor stays fixed.
43
+ //
44
+ // unlike @floating-ui/react, we do NOT add a documentElement mouseleave
45
+ // listener, which fixes the window-blur-closing-popover bug.
46
+ // debug overlay — renders polygon + trough as SVG on top of everything
47
+ let debugSvg: SVGSVGElement | null = null
48
+ function debugDrawPolygon(
49
+ polygon: Point[],
50
+ trough: Point[],
51
+ cursor: Point,
52
+ anchor: Point
53
+ ) {
54
+ if (!debugSvg) {
55
+ debugSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
56
+ debugSvg.id = '__safe-polygon-debug'
57
+ Object.assign(debugSvg.style, {
58
+ position: 'fixed',
59
+ inset: '0',
60
+ width: '100vw',
61
+ height: '100vh',
62
+ pointerEvents: 'none',
63
+ zIndex: '999999',
64
+ })
65
+ document.body.appendChild(debugSvg)
66
+ }
67
+ debugSvg.innerHTML = ''
68
+
69
+ // trough rectangle (blue)
70
+ if (trough.length) {
71
+ const troughEl = document.createElementNS('http://www.w3.org/2000/svg', 'polygon')
72
+ troughEl.setAttribute('points', trough.map((p) => p.join(',')).join(' '))
73
+ troughEl.setAttribute('fill', 'rgba(0,100,255,0.15)')
74
+ troughEl.setAttribute('stroke', 'rgba(0,100,255,0.6)')
75
+ troughEl.setAttribute('stroke-width', '1')
76
+ debugSvg.appendChild(troughEl)
77
+ }
78
+
79
+ // safe polygon (red)
80
+ if (polygon.length) {
81
+ const polyEl = document.createElementNS('http://www.w3.org/2000/svg', 'polygon')
82
+ polyEl.setAttribute('points', polygon.map((p) => p.join(',')).join(' '))
83
+ polyEl.setAttribute('fill', 'rgba(255,50,50,0.2)')
84
+ polyEl.setAttribute('stroke', 'rgba(255,50,50,0.8)')
85
+ polyEl.setAttribute('stroke-width', '1.5')
86
+ debugSvg.appendChild(polyEl)
87
+ }
88
+
89
+ // anchor point (green circle)
90
+ const anchorCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle')
91
+ anchorCircle.setAttribute('cx', String(anchor[0]))
92
+ anchorCircle.setAttribute('cy', String(anchor[1]))
93
+ anchorCircle.setAttribute('r', '5')
94
+ anchorCircle.setAttribute('fill', 'lime')
95
+ anchorCircle.setAttribute('stroke', 'darkgreen')
96
+ anchorCircle.setAttribute('stroke-width', '1.5')
97
+ debugSvg.appendChild(anchorCircle)
98
+
99
+ // cursor point (yellow circle)
100
+ const cursorCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle')
101
+ cursorCircle.setAttribute('cx', String(cursor[0]))
102
+ cursorCircle.setAttribute('cy', String(cursor[1]))
103
+ cursorCircle.setAttribute('r', '4')
104
+ cursorCircle.setAttribute('fill', 'yellow')
105
+ cursorCircle.setAttribute('stroke', 'orange')
106
+ cursorCircle.setAttribute('stroke-width', '1.5')
107
+ debugSvg.appendChild(cursorCircle)
108
+ }
109
+
110
+ function debugClear() {
111
+ if (debugSvg) {
112
+ debugSvg.remove()
113
+ debugSvg = null
114
+ }
115
+ }
116
+
117
+ export function safePolygon(options: SafePolygonOptions = {}): HandleCloseFn {
118
+ const {
119
+ buffer = 0.5,
120
+ blockPointerEvents = false,
121
+ requireIntent = true,
122
+ __debug = false,
123
+ } = options
124
+
125
+ const timeoutRef = { current: -1 }
126
+
127
+ let hasLanded = false
128
+ let lastX: number | null = null
129
+ let lastY: number | null = null
130
+ let lastCursorTime = typeof performance !== 'undefined' ? performance.now() : 0
131
+
132
+ function getCursorSpeed(x: number, y: number): number | null {
133
+ const currentTime = performance.now()
134
+ const elapsedTime = currentTime - lastCursorTime
135
+
136
+ if (lastX === null || lastY === null || elapsedTime === 0) {
137
+ lastX = x
138
+ lastY = y
139
+ lastCursorTime = currentTime
140
+ return null
141
+ }
142
+
143
+ const deltaX = x - lastX
144
+ const deltaY = y - lastY
145
+ const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY)
146
+ const speed = distance / elapsedTime
147
+
148
+ lastX = x
149
+ lastY = y
150
+ lastCursorTime = currentTime
151
+
152
+ return speed
153
+ }
154
+
155
+ // called once on mouseleave. x, y = cursor position when leaving.
156
+ // returns the mousemove handler that checks each subsequent position
157
+ // against the polygon anchored at (x, y).
158
+ const fn: HandleCloseFn = ({ x, y, placement, elements, onClose }) => {
159
+ // reset on each new handler creation — each leave starts a fresh session
160
+ hasLanded = false
161
+ lastX = null
162
+ lastY = null
163
+
164
+ return function onMouseMove(event: MouseEvent) {
165
+ function close() {
166
+ clearTimeoutIfSet(timeoutRef)
167
+ onClose()
168
+ }
169
+
170
+ clearTimeoutIfSet(timeoutRef)
171
+
172
+ const domReference = elements.domReference ?? elements.reference
173
+
174
+ if (
175
+ !domReference ||
176
+ !elements.floating ||
177
+ placement == null ||
178
+ x == null ||
179
+ y == null
180
+ ) {
181
+ return
182
+ }
183
+
184
+ const { clientX, clientY } = event
185
+ const clientPoint: Point = [clientX, clientY]
186
+ const target = getTarget(event) as Element | null
187
+ const isLeave = event.type === 'mouseleave'
188
+ const isOverFloatingEl = contains(elements.floating, target)
189
+ const isOverReferenceEl = contains(domReference, target)
190
+ const refRect = domReference.getBoundingClientRect()
191
+ const rect = elements.floating.getBoundingClientRect()
192
+ const side = placement.split('-')[0] as Side
193
+
194
+ // x, y are from the closure — the position when cursor LEFT the reference.
195
+ // cursorLeaveFromRight/Bottom determine which side of the floating el
196
+ // the leave point is on, to shape the polygon correctly.
197
+ const cursorLeaveFromRight = x > rect.right - rect.width / 2
198
+ const cursorLeaveFromBottom = y > rect.bottom - rect.height / 2
199
+ const isOverReferenceRect = isInside(clientPoint, refRect)
200
+ const isFloatingWider = rect.width > refRect.width
201
+ const isFloatingTaller = rect.height > refRect.height
202
+ const left = (isFloatingWider ? refRect : rect).left
203
+ const right = (isFloatingWider ? refRect : rect).right
204
+ const top = (isFloatingTaller ? refRect : rect).top
205
+ const bottom = (isFloatingTaller ? refRect : rect).bottom
206
+
207
+ if (isOverFloatingEl) {
208
+ hasLanded = true
209
+
210
+ if (!isLeave) {
211
+ return
212
+ }
213
+ }
214
+
215
+ if (isOverReferenceEl) {
216
+ hasLanded = false
217
+ }
218
+
219
+ if (isOverReferenceEl && !isLeave) {
220
+ hasLanded = true
221
+ return
222
+ }
223
+
224
+ // cursor in reference bounding box but outside DOM element (rounded corners)
225
+ if (!isOverReferenceEl && isOverReferenceRect && !isLeave) {
226
+ return
227
+ }
228
+
229
+ // prevent overlapping floating element from being stuck in an open-close
230
+ // loop: https://github.com/floating-ui/floating-ui/issues/1910
231
+ if (
232
+ isLeave &&
233
+ event.relatedTarget &&
234
+ contains(elements.floating, event.relatedTarget as Element)
235
+ ) {
236
+ return
237
+ }
238
+
239
+ // if the pointer is leaving from the opposite side, the "buffer" logic
240
+ // creates a point where the floating element remains open, but should be
241
+ // ignored. a constant of 1 handles floating point rounding errors.
242
+ if (
243
+ (side === 'top' && y >= refRect.bottom - 1) ||
244
+ (side === 'bottom' && y <= refRect.top + 1) ||
245
+ (side === 'left' && x >= refRect.right - 1) ||
246
+ (side === 'right' && x <= refRect.left + 1)
247
+ ) {
248
+ return close()
249
+ }
250
+
251
+ // ignore when the cursor is within the rectangular trough between the
252
+ // two elements. since the polygon is created from the cursor point,
253
+ // which can start beyond the ref element's edge, traversing back and
254
+ // forth from the ref to the floating element can cause it to close. this
255
+ // ensures it always remains open in that case.
256
+ let rectPoly: Point[] = []
257
+
258
+ switch (side) {
259
+ case 'top':
260
+ rectPoly = [
261
+ [left, refRect.top + 1],
262
+ [left, rect.bottom - 1],
263
+ [right, rect.bottom - 1],
264
+ [right, refRect.top + 1],
265
+ ]
266
+ break
267
+ case 'bottom':
268
+ rectPoly = [
269
+ [left, rect.top + 1],
270
+ [left, refRect.bottom - 1],
271
+ [right, refRect.bottom - 1],
272
+ [right, rect.top + 1],
273
+ ]
274
+ break
275
+ case 'left':
276
+ rectPoly = [
277
+ [rect.right - 1, bottom],
278
+ [rect.right - 1, top],
279
+ [refRect.left + 1, top],
280
+ [refRect.left + 1, bottom],
281
+ ]
282
+ break
283
+ case 'right':
284
+ rectPoly = [
285
+ [refRect.right - 1, bottom],
286
+ [refRect.right - 1, top],
287
+ [rect.left + 1, top],
288
+ [rect.left + 1, bottom],
289
+ ]
290
+ break
291
+ }
292
+
293
+ // getPolygon uses the closure's (x, y) — the LEAVE position — as the
294
+ // polygon anchor point, NOT the current cursor position. this creates
295
+ // a stable triangular/trapezoidal safe zone from the leave point toward
296
+ // the floating element's edges.
297
+ function getPolygon([x, y]: Point): Array<Point> {
298
+ switch (side) {
299
+ case 'top': {
300
+ const cursorPointOne: Point = [
301
+ isFloatingWider
302
+ ? x + buffer / 2
303
+ : cursorLeaveFromRight
304
+ ? x + buffer * 4
305
+ : x - buffer * 4,
306
+ y + buffer + 1,
307
+ ]
308
+ const cursorPointTwo: Point = [
309
+ isFloatingWider
310
+ ? x - buffer / 2
311
+ : cursorLeaveFromRight
312
+ ? x + buffer * 4
313
+ : x - buffer * 4,
314
+ y + buffer + 1,
315
+ ]
316
+ const commonPoints: [Point, Point] = [
317
+ [
318
+ rect.left,
319
+ cursorLeaveFromRight
320
+ ? rect.bottom - buffer
321
+ : isFloatingWider
322
+ ? rect.bottom - buffer
323
+ : rect.top,
324
+ ],
325
+ [
326
+ rect.right,
327
+ cursorLeaveFromRight
328
+ ? isFloatingWider
329
+ ? rect.bottom - buffer
330
+ : rect.top
331
+ : rect.bottom - buffer,
332
+ ],
333
+ ]
334
+
335
+ return [cursorPointOne, cursorPointTwo, ...commonPoints]
336
+ }
337
+ case 'bottom': {
338
+ const cursorPointOne: Point = [
339
+ isFloatingWider
340
+ ? x + buffer / 2
341
+ : cursorLeaveFromRight
342
+ ? x + buffer * 4
343
+ : x - buffer * 4,
344
+ y - buffer,
345
+ ]
346
+ const cursorPointTwo: Point = [
347
+ isFloatingWider
348
+ ? x - buffer / 2
349
+ : cursorLeaveFromRight
350
+ ? x + buffer * 4
351
+ : x - buffer * 4,
352
+ y - buffer,
353
+ ]
354
+ const commonPoints: [Point, Point] = [
355
+ [
356
+ rect.left,
357
+ cursorLeaveFromRight
358
+ ? rect.top + buffer
359
+ : isFloatingWider
360
+ ? rect.top + buffer
361
+ : rect.bottom,
362
+ ],
363
+ [
364
+ rect.right,
365
+ cursorLeaveFromRight
366
+ ? isFloatingWider
367
+ ? rect.top + buffer
368
+ : rect.bottom
369
+ : rect.top + buffer,
370
+ ],
371
+ ]
372
+
373
+ return [cursorPointOne, cursorPointTwo, ...commonPoints]
374
+ }
375
+ case 'left': {
376
+ const cursorPointOne: Point = [
377
+ x + buffer + 1,
378
+ isFloatingTaller
379
+ ? y + buffer / 2
380
+ : cursorLeaveFromBottom
381
+ ? y + buffer * 4
382
+ : y - buffer * 4,
383
+ ]
384
+ const cursorPointTwo: Point = [
385
+ x + buffer + 1,
386
+ isFloatingTaller
387
+ ? y - buffer / 2
388
+ : cursorLeaveFromBottom
389
+ ? y + buffer * 4
390
+ : y - buffer * 4,
391
+ ]
392
+ const commonPoints: [Point, Point] = [
393
+ [
394
+ cursorLeaveFromBottom
395
+ ? rect.right - buffer
396
+ : isFloatingTaller
397
+ ? rect.right - buffer
398
+ : rect.left,
399
+ rect.top,
400
+ ],
401
+ [
402
+ cursorLeaveFromBottom
403
+ ? isFloatingTaller
404
+ ? rect.right - buffer
405
+ : rect.left
406
+ : rect.right - buffer,
407
+ rect.bottom,
408
+ ],
409
+ ]
410
+
411
+ return [...commonPoints, cursorPointOne, cursorPointTwo]
412
+ }
413
+ case 'right': {
414
+ const cursorPointOne: Point = [
415
+ x - buffer,
416
+ isFloatingTaller
417
+ ? y + buffer / 2
418
+ : cursorLeaveFromBottom
419
+ ? y + buffer * 4
420
+ : y - buffer * 4,
421
+ ]
422
+ const cursorPointTwo: Point = [
423
+ x - buffer,
424
+ isFloatingTaller
425
+ ? y - buffer / 2
426
+ : cursorLeaveFromBottom
427
+ ? y + buffer * 4
428
+ : y - buffer * 4,
429
+ ]
430
+ const commonPoints: [Point, Point] = [
431
+ [
432
+ cursorLeaveFromBottom
433
+ ? rect.left + buffer
434
+ : isFloatingTaller
435
+ ? rect.left + buffer
436
+ : rect.right,
437
+ rect.top,
438
+ ],
439
+ [
440
+ cursorLeaveFromBottom
441
+ ? isFloatingTaller
442
+ ? rect.left + buffer
443
+ : rect.right
444
+ : rect.left + buffer,
445
+ rect.bottom,
446
+ ],
447
+ ]
448
+
449
+ return [cursorPointOne, cursorPointTwo, ...commonPoints]
450
+ }
451
+ }
452
+ }
453
+
454
+ const poly = getPolygon([x, y])
455
+
456
+ if (__debug) {
457
+ debugDrawPolygon(poly, rectPoly, clientPoint, [x, y])
458
+ }
459
+
460
+ if (isPointInPolygon([clientX, clientY], rectPoly)) {
461
+ return
462
+ }
463
+
464
+ if (hasLanded && !isOverReferenceRect) {
465
+ if (__debug) debugClear()
466
+ return close()
467
+ }
468
+
469
+ // polygon check first — inside polygon = safe.
470
+ // the polygon geometry itself limits the safe zone to valid cursor
471
+ // paths toward the floating element, so no timeout is needed here.
472
+ // a previous 40ms requireIntent timeout caused premature closures
473
+ // during natural mouse pauses (humans routinely pause >40ms while
474
+ // moving diagonally from trigger to content).
475
+ if (isPointInPolygon([clientX, clientY], poly)) {
476
+ return
477
+ }
478
+
479
+ // speed check only applies outside polygon
480
+ if (!isLeave && requireIntent) {
481
+ const cursorSpeed = getCursorSpeed(clientX, clientY)
482
+ const cursorSpeedThreshold = 0.1
483
+ if (cursorSpeed !== null && cursorSpeed < cursorSpeedThreshold) {
484
+ if (__debug) debugClear()
485
+ return close()
486
+ }
487
+ }
488
+
489
+ // outside polygon — close
490
+ if (__debug) debugClear()
491
+ close()
492
+ }
493
+ }
494
+
495
+ fn.__options = {
496
+ blockPointerEvents,
497
+ }
498
+
499
+ return fn
500
+ }