diff --git a/frontend/src/app/components/Onboarding/ac/AgenticCursor.tsx b/frontend/src/app/components/Onboarding/ac/AgenticCursor.tsx index 3f0bc7d7..5b057547 100644 --- a/frontend/src/app/components/Onboarding/ac/AgenticCursor.tsx +++ b/frontend/src/app/components/Onboarding/ac/AgenticCursor.tsx @@ -67,10 +67,10 @@ const AgenticCursor = forwardRef((_props, ref) => { const trackerRef = useRef<{ stop: () => void } | null>(null); - // Mirrored into cursorStore so popups follow without re-running through Framer's animation pipeline. - const writePos = (x: number, y: number, vis = true) => { + // Mirrored into cursorStore so popups follow without re-running through Framer's animation pipeline. `instant` controls the Windows CSS-transition fallback: true = snap (tracking), false = ease (moveTo/fadeOut). No-op on Mac. + const writePos = (x: number, y: number, vis = true, instant = true) => { posRef.current = { x, y }; - cursorStore.set({ x, y, visible: vis }); + cursorStore.set({ x, y, visible: vis, instant }); }; const stopTrackingInternal = () => { @@ -100,17 +100,18 @@ const AgenticCursor = forwardRef((_props, ref) => { async moveTo(x, y, transition) { // Stop prior tracker so it doesn't snap the cursor back to its old anchor mid-animation. stopTrackingInternal(); + // instant=false so the Windows CSS-transition eases here (mirrors Mac's spring controls.start). + writePos(x, y, true, false); await controls.start({ x, y, transition: transition ?? SPRING, }); - writePos(x, y, true); }, async fadeOut(to) { stopTrackingInternal(); + writePos(to.x, to.y, true, false); await controls.start({ x: to.x, y: to.y, transition: SPRING }); - writePos(to.x, to.y, true); await controls.start({ opacity: 0, scale: 0.5, @@ -262,8 +263,8 @@ const AgenticCursor = forwardRef((_props, ref) => { ...(IS_WIN ? { transform: `translate(${storePos.x}px, ${storePos.y}px)`, - // Closest CSS approximation of the Mac spring (stiffness 260, damping 26): a softly easing ~420ms cubic-bezier. Without this the cursor teleports because the shim strips Framer's spring runtime. - transition: 'transform 420ms cubic-bezier(0.22, 1, 0.36, 1)', + // Closest CSS approximation of the Mac spring (stiffness 260, damping 26): a softly easing ~420ms cubic-bezier for moveTo/fadeOut. Tracking sets instant=true so the cursor snaps to its target each frame instead of perpetually lagging behind. + transition: storePos.instant ? 'none' : 'transform 420ms cubic-bezier(0.22, 1, 0.36, 1)', willChange: 'transform', } : null), diff --git a/frontend/src/app/components/Onboarding/ac/cursorStore.ts b/frontend/src/app/components/Onboarding/ac/cursorStore.ts index b85c4a26..c24fb575 100644 --- a/frontend/src/app/components/Onboarding/ac/cursorStore.ts +++ b/frontend/src/app/components/Onboarding/ac/cursorStore.ts @@ -6,9 +6,11 @@ interface CursorPos { x: number; y: number; visible: boolean; + // Windows-only: the motionWin shim strips Framer's spring, so the cursor wrapper eases via CSS transition. `instant` tells it to disable the transition for this update — set true while tracking a (mostly stationary) element so the cursor snaps like Mac's controls.set, false for moveTo/fadeOut so it eases like controls.start. Ignored on Mac (Framer drives the motion). + instant: boolean; } -let state: CursorPos = { x: 0, y: 0, visible: false }; +let state: CursorPos = { x: 0, y: 0, visible: false, instant: true }; let pendingState: CursorPos | null = null; const listeners = new Set<() => void>();