66 lines
2.1 KiB
TypeScript
66 lines
2.1 KiB
TypeScript
'use client'
|
|
|
|
// MagneticButton — isolated client component (rerender-no-inline-components rule)
|
|
// Never define this inside HeroSection or any parent component
|
|
// Uses framer-motion useMotionValue + useSpring for magnetic pull
|
|
// useMotionValue lives OUTSIDE render cycle — no useState performance collapse
|
|
|
|
import { useRef } from 'react'
|
|
import { motion, useMotionValue, useSpring } from 'framer-motion'
|
|
|
|
interface MagneticButtonProps {
|
|
children: React.ReactNode
|
|
href: string
|
|
className?: string
|
|
}
|
|
|
|
export function MagneticButton({ children, href, className }: MagneticButtonProps) {
|
|
const ref = useRef<HTMLAnchorElement>(null)
|
|
|
|
// useMotionValue is outside render — no re-renders on mouse move
|
|
const x = useMotionValue(0)
|
|
const y = useMotionValue(0)
|
|
const springX = useSpring(x, { stiffness: 150, damping: 15 })
|
|
const springY = useSpring(y, { stiffness: 150, damping: 15 })
|
|
|
|
const handleMouseMove = (e: React.MouseEvent<HTMLAnchorElement>) => {
|
|
if (!ref.current) return
|
|
const rect = ref.current.getBoundingClientRect()
|
|
const cx = rect.left + rect.width / 2
|
|
const cy = rect.top + rect.height / 2
|
|
x.set((e.clientX - cx) * 0.3)
|
|
y.set((e.clientY - cy) * 0.3)
|
|
}
|
|
|
|
const handleMouseLeave = () => {
|
|
x.set(0)
|
|
y.set(0)
|
|
}
|
|
|
|
return (
|
|
<motion.a
|
|
ref={ref}
|
|
href={href}
|
|
style={{ x: springX, y: springY }}
|
|
onMouseMove={handleMouseMove}
|
|
onMouseLeave={handleMouseLeave}
|
|
whileTap={{ scale: 0.97 }}
|
|
className={[
|
|
'hero-cta inline-flex items-center gap-3 rounded-full',
|
|
'bg-[var(--color-primary)] px-7 py-4',
|
|
'text-sm font-medium text-white',
|
|
'transition-colors duration-300 hover:bg-[var(--color-primary)]/90',
|
|
className,
|
|
]
|
|
.filter(Boolean)
|
|
.join(' ')}
|
|
>
|
|
{children}
|
|
{/* Button-in-button trailing icon — icon wrapped in its own circle */}
|
|
<span className="flex h-7 w-7 items-center justify-center rounded-full bg-white/15 transition-transform duration-300 group-hover:translate-x-0.5 group-hover:-translate-y-0.5">
|
|
↗
|
|
</span>
|
|
</motion.a>
|
|
)
|
|
}
|