All checks were successful
Build & Deploy / 🔍 Prepare (push) Successful in 42s
Build & Deploy / 🧪 QA (push) Successful in 5m17s
Build & Deploy / 🏗️ Build (push) Successful in 8m36s
Build & Deploy / 🚀 Deploy (push) Successful in 17s
Build & Deploy / 🧪 Smoke Test (push) Successful in 53s
Build & Deploy / ⚡ Lighthouse (push) Successful in 7m38s
Build & Deploy / 🔔 Notify (push) Successful in 2s
93 lines
3.2 KiB
TypeScript
93 lines
3.2 KiB
TypeScript
'use client';
|
|
|
|
import React, { useEffect, useState } from 'react';
|
|
import { m, LazyMotion, AnimatePresence } from 'framer-motion';
|
|
import { useRecordMode } from './RecordModeContext';
|
|
|
|
export function PlaybackCursor() {
|
|
const { isPlaying, cursorPosition, isClicking } = useRecordMode();
|
|
const [scrollOffset, setScrollOffset] = useState({ x: 0, y: 0 });
|
|
|
|
// Track scroll so cursor stays locked to the correct element
|
|
useEffect(() => {
|
|
if (!isPlaying) return;
|
|
|
|
const handleScroll = () => {
|
|
setScrollOffset({ x: window.scrollX, y: window.scrollY });
|
|
};
|
|
|
|
handleScroll(); // Init
|
|
window.addEventListener('scroll', handleScroll, { passive: true });
|
|
return () => window.removeEventListener('scroll', handleScroll);
|
|
}, [isPlaying]);
|
|
|
|
if (!isPlaying) return null;
|
|
|
|
return (
|
|
<LazyMotion strict features={() => import('@/lib/framer-features').then(res => res.default)}>
|
|
<m.div
|
|
className="fixed z-[10000] pointer-events-none"
|
|
animate={{
|
|
x: cursorPosition.x,
|
|
y: cursorPosition.y,
|
|
scale: isClicking ? 0.8 : 1,
|
|
rotateX: isClicking ? 15 : 0,
|
|
rotateY: isClicking ? -15 : 0,
|
|
}}
|
|
transition={{
|
|
x: { type: 'spring', damping: 30, stiffness: 250, mass: 0.5 },
|
|
y: { type: 'spring', damping: 30, stiffness: 250, mass: 0.5 },
|
|
scale: { type: 'spring', damping: 15, stiffness: 400 },
|
|
rotateX: { type: 'spring', damping: 15, stiffness: 400 },
|
|
rotateY: { type: 'spring', damping: 15, stiffness: 400 },
|
|
}}
|
|
style={{ perspective: '1000px' }}
|
|
>
|
|
<AnimatePresence>
|
|
{isClicking && (
|
|
<m.div
|
|
initial={{ scale: 0.5, opacity: 0 }}
|
|
animate={{ scale: 2.5, opacity: 0 }}
|
|
exit={{ opacity: 0 }}
|
|
transition={{ duration: 0.4, ease: 'easeOut' }}
|
|
className="absolute inset-0 rounded-full border-2 border-[#82ed20] blur-[1px]"
|
|
/>
|
|
)}
|
|
</AnimatePresence>
|
|
|
|
{/* Outer Pulse Ring */}
|
|
<div
|
|
className={`absolute -inset-3 rounded-full bg-[#82ed20]/10 ${isClicking ? 'scale-150 opacity-0' : 'animate-ping'} transition-all duration-300`}
|
|
/>
|
|
|
|
{/* Visual Cursor */}
|
|
<div className="relative">
|
|
{/* Soft Glow */}
|
|
<div
|
|
className={`absolute -inset-2 bg-[#82ed20]/20 rounded-full blur-md transition-all ${isClicking ? 'bg-[#82ed20]/50 blur-xl' : ''}`}
|
|
/>
|
|
|
|
{/* Pointer Arrow */}
|
|
<svg
|
|
width="24"
|
|
height="24"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
className={`drop-shadow-[0_2px_8px_rgba(130,237,32,0.5)] transition-transform ${isClicking ? 'translate-y-0.5' : ''}`}
|
|
>
|
|
<path
|
|
d="M3 3L10.07 19.97L12.58 12.58L19.97 10.07L3 3Z"
|
|
fill={isClicking ? '#82ed20' : 'white'}
|
|
stroke="black"
|
|
strokeWidth="1.5"
|
|
strokeLinejoin="round"
|
|
className="transition-colors duration-150"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
</m.div>
|
|
</LazyMotion>
|
|
);
|
|
}
|