@@ -113,6 +113,33 @@ export function SiteMotion() {
113113
114114 // Recompute trigger positions once the display font has settled.
115115 ScrollTrigger . refresh ( ) ;
116+
117+ // 5) Magnetic primary CTAs — they pull toward the cursor. Pointer
118+ // devices only; quickTo runs off the React render cycle.
119+ const magnetCleanups : Array < ( ) => void > = [ ] ;
120+ if ( window . matchMedia ( '(pointer: fine)' ) . matches ) {
121+ gsap . utils . toArray < HTMLElement > ( '.btn-primary' ) . forEach ( ( btn ) => {
122+ const xTo = gsap . quickTo ( btn , 'x' , { duration : 0.5 , ease : 'power3' } ) ;
123+ const yTo = gsap . quickTo ( btn , 'y' , { duration : 0.5 , ease : 'power3' } ) ;
124+ const onMove = ( e : PointerEvent ) => {
125+ const r = btn . getBoundingClientRect ( ) ;
126+ xTo ( ( e . clientX - ( r . left + r . width / 2 ) ) * 0.35 ) ;
127+ yTo ( ( e . clientY - ( r . top + r . height / 2 ) ) * 0.5 ) ;
128+ } ;
129+ const onLeave = ( ) => {
130+ xTo ( 0 ) ;
131+ yTo ( 0 ) ;
132+ } ;
133+ btn . addEventListener ( 'pointermove' , onMove ) ;
134+ btn . addEventListener ( 'pointerleave' , onLeave ) ;
135+ magnetCleanups . push ( ( ) => {
136+ btn . removeEventListener ( 'pointermove' , onMove ) ;
137+ btn . removeEventListener ( 'pointerleave' , onLeave ) ;
138+ } ) ;
139+ } ) ;
140+ }
141+
142+ return ( ) => magnetCleanups . forEach ( ( fn ) => fn ( ) ) ;
116143 } ) ;
117144
118145 cleanup = ( ) => mm . revert ( ) ;
0 commit comments