80 lines
2.4 KiB
TypeScript
80 lines
2.4 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useState } from 'react'
|
|
|
|
export function CursorEffect() {
|
|
const [position, setPosition] = useState({ x: 0, y: 0 })
|
|
const [isHovering, setIsHovering] = useState(false)
|
|
const [trails, setTrails] = useState<Array<{ x: number; y: number; id: number }>>([])
|
|
|
|
useEffect(() => {
|
|
let trailId = 0
|
|
|
|
const handleMouseMove = (e: MouseEvent) => {
|
|
setPosition({ x: e.clientX, y: e.clientY })
|
|
|
|
// Add trail
|
|
const newTrail = { x: e.clientX, y: e.clientY, id: trailId++ }
|
|
setTrails(prev => [...prev.slice(-8), newTrail])
|
|
|
|
// Check if hovering over interactive element
|
|
const target = e.target as HTMLElement
|
|
const isInteractive = target.closest('a, button, [role="button"]')
|
|
setIsHovering(!!isInteractive)
|
|
}
|
|
|
|
window.addEventListener('mousemove', handleMouseMove)
|
|
return () => window.removeEventListener('mousemove', handleMouseMove)
|
|
}, [])
|
|
|
|
return (
|
|
<>
|
|
{/* Main cursor */}
|
|
<div
|
|
className="pointer-events-none fixed z-[9999] mix-blend-difference"
|
|
style={{
|
|
left: `${position.x}px`,
|
|
top: `${position.y}px`,
|
|
transform: 'translate(-50%, -50%)',
|
|
transition: 'width 0.2s, height 0.2s',
|
|
}}
|
|
>
|
|
<div
|
|
className={`rounded-full bg-primary transition-all duration-200 ${
|
|
isHovering ? 'h-12 w-12' : 'h-6 w-6'
|
|
}`}
|
|
/>
|
|
</div>
|
|
|
|
{/* Trail effect */}
|
|
{trails.map((trail, index) => (
|
|
<div
|
|
key={trail.id}
|
|
className="pointer-events-none fixed z-[9998] rounded-full bg-accent mix-blend-screen"
|
|
style={{
|
|
left: `${trail.x}px`,
|
|
top: `${trail.y}px`,
|
|
transform: 'translate(-50%, -50%)',
|
|
width: `${4 + index}px`,
|
|
height: `${4 + index}px`,
|
|
opacity: (index + 1) / trails.length * 0.3,
|
|
transition: 'opacity 0.5s',
|
|
}}
|
|
/>
|
|
))}
|
|
|
|
{/* Outer ring */}
|
|
<div
|
|
className="pointer-events-none fixed z-[9997] rounded-full border-2 border-primary/30 transition-all duration-300"
|
|
style={{
|
|
left: `${position.x}px`,
|
|
top: `${position.y}px`,
|
|
transform: 'translate(-50%, -50%)',
|
|
width: isHovering ? '64px' : '48px',
|
|
height: isHovering ? '64px' : '48px',
|
|
}}
|
|
/>
|
|
</>
|
|
)
|
|
}
|