skills/website/templates/magnetic-button.tsx

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>
)
}