11import {
2+ Component ,
23 ComponentPropsWithoutRef ,
34 Fragment ,
45 ReactNode ,
@@ -16,91 +17,66 @@ interface MousePosition {
1617 y : number ;
1718}
1819const MousePositionContext = createContext < MousePosition | undefined > ( undefined ) ;
19- export function MousePositionProvider ( { children } : { children : ReactNode } ) {
20+ export function MousePositionProvider ( {
21+ children,
22+ recalculateTrigger,
23+ } : {
24+ children : ReactNode ;
25+ recalculateTrigger ?: unknown ;
26+ } ) {
2027 const ref = useRef < HTMLDivElement > ( null ) ;
2128 const [ position , setPosition ] = useState < MousePosition | undefined > ( undefined ) ;
22- const lastClient = useRef < { clientX : number ; clientY : number } | null > ( null ) ;
23- const rafId = useRef < number | null > ( null ) ;
29+ const lastMouseCoordsRef = useRef < { clientX : number ; clientY : number } | null > ( null ) ;
2430
25- const computeFromClient = useCallback ( ( clientX : number , clientY : number ) => {
26- if ( ! ref . current ) {
27- setPosition ( undefined ) ;
28- return ;
29- }
31+ const handleMouseMove = useCallback (
32+ ( e : React . MouseEvent ) => {
33+ lastMouseCoordsRef . current = { clientX : e . clientX , clientY : e . clientY } ;
3034
31- const { top, left, width, height } = ref . current . getBoundingClientRect ( ) ;
32- const x = ( clientX - left ) / width ;
33- const y = ( clientY - top ) / height ;
35+ if ( ! ref . current ) {
36+ setPosition ( undefined ) ;
37+ return ;
38+ }
3439
35- if ( x < 0 || x > 1 || y < 0 || y > 1 ) {
36- setPosition ( undefined ) ;
37- return ;
38- }
40+ const { top, left, width, height } = ref . current . getBoundingClientRect ( ) ;
41+ const x = ( e . clientX - left ) / width ;
42+ const y = ( e . clientY - top ) / height ;
3943
40- setPosition ( { x, y } ) ;
41- } , [ ] ) ;
44+ if ( x < 0 || x > 1 || y < 0 || y > 1 ) {
45+ setPosition ( undefined ) ;
46+ return ;
47+ }
4248
43- const handleMouseMove = useCallback (
44- ( e : React . MouseEvent ) => {
45- lastClient . current = { clientX : e . clientX , clientY : e . clientY } ;
46- computeFromClient ( e . clientX , e . clientY ) ;
49+ setPosition ( { x, y } ) ;
4750 } ,
48- [ computeFromClient ]
51+ [ ref . current ]
4952 ) ;
5053
51- // Recalculate the relative position when the container resizes or the window/ancestors scroll.
54+ // Recalculate position when trigger changes (e.g., panel opens/closes)
55+ // Use requestAnimationFrame to wait for the DOM layout to complete
5256 useEffect ( ( ) => {
53- if ( ! ref . current ) return ;
54-
55- const ro = new ResizeObserver ( ( ) => {
56- const lc = lastClient . current ;
57- if ( lc ) computeFromClient ( lc . clientX , lc . clientY ) ;
58- } ) ;
59- ro . observe ( ref . current ) ;
60-
61- const onRecalc = ( ) => {
62- const lc = lastClient . current ;
63- if ( lc ) computeFromClient ( lc . clientX , lc . clientY ) ;
64- } ;
57+ if ( ! lastMouseCoordsRef . current ) {
58+ return ;
59+ }
6560
66- window . addEventListener ( "resize" , onRecalc ) ;
67- // Use capture to catch scroll on any ancestor that impacts bounding rect
68- window . addEventListener ( "scroll" , onRecalc , true ) ;
61+ const rafId = requestAnimationFrame ( ( ) => {
62+ if ( ! ref . current || ! lastMouseCoordsRef . current ) {
63+ return ;
64+ }
6965
70- return ( ) => {
71- ro . disconnect ( ) ;
72- window . removeEventListener ( "resize" , onRecalc ) ;
73- window . removeEventListener ( "scroll" , onRecalc , true ) ;
74- } ;
75- } , [ computeFromClient ] ) ;
66+ const { top, left, width, height } = ref . current . getBoundingClientRect ( ) ;
67+ const x = ( lastMouseCoordsRef . current . clientX - left ) / width ;
68+ const y = ( lastMouseCoordsRef . current . clientY - top ) / height ;
7669
77- useEffect ( ( ) => {
78- if ( position === undefined || ! lastClient . current || ! ref . current ) return ;
79-
80- const isAnimating = ( ) => {
81- if ( ! ref . current ) return false ;
82- const styles = window . getComputedStyle ( ref . current ) ;
83- return styles . transition !== "none" || styles . animation !== "none" ;
84- } ;
85-
86- const tick = ( ) => {
87- const lc = lastClient . current ;
88- if ( lc ) {
89- computeFromClient ( lc . clientX , lc . clientY ) ;
90- if ( isAnimating ( ) ) {
91- rafId . current = requestAnimationFrame ( tick ) ;
92- } else {
93- rafId . current = null ;
94- }
70+ if ( x < 0 || x > 1 || y < 0 || y > 1 ) {
71+ setPosition ( undefined ) ;
72+ return ;
9573 }
96- } ;
9774
98- rafId . current = requestAnimationFrame ( tick ) ;
99- return ( ) => {
100- if ( rafId . current !== null ) cancelAnimationFrame ( rafId . current ) ;
101- rafId . current = null ;
102- } ;
103- } , [ position , computeFromClient ] ) ;
75+ setPosition ( { x, y } ) ;
76+ } ) ;
77+
78+ return ( ) => cancelAnimationFrame ( rafId ) ;
79+ } , [ recalculateTrigger ] ) ;
10480
10581 return (
10682 < div
@@ -144,6 +120,8 @@ export type RootProps = {
144120 maxWidth : number ;
145121 children ?: ReactNode ;
146122 className ?: string ;
123+ /** When this value changes, recalculate the mouse position (useful when panels resize) */
124+ recalculateTrigger ?: unknown ;
147125} ;
148126
149127/** The main element that determines the dimensions for all sub-elements */
@@ -155,6 +133,7 @@ export function Root({
155133 maxWidth,
156134 children,
157135 className,
136+ recalculateTrigger,
158137} : RootProps ) {
159138 const pixelWidth = calculatePixelWidth ( minWidth , maxWidth , scale ) ;
160139
@@ -167,7 +146,9 @@ export function Root({
167146 width : `${ pixelWidth } px` ,
168147 } }
169148 >
170- < MousePositionProvider > { children } </ MousePositionProvider >
149+ < MousePositionProvider recalculateTrigger = { recalculateTrigger } >
150+ { children }
151+ </ MousePositionProvider >
171152 </ div >
172153 </ TimelineContext . Provider >
173154 ) ;
0 commit comments