Skip to content

Commit 3e6eb4f

Browse files
author
teycir
committed
feat: add magnetic hover effect to Button component
Extend ButtonProps with HTMLMotionProps, add mouse event handlers for subtle magnetic pull using springs, and update hover animations for enhanced interactivity. This creates a more engaging user experience with smooth, weak magnetic attraction on mouse movement.
1 parent 3d9b36f commit 3e6eb4f

4 files changed

Lines changed: 469 additions & 362 deletions

File tree

app/components/Button.tsx

Lines changed: 58 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,80 @@
11
'use client';
22

3-
interface ButtonProps {
3+
import { motion, useMotionValue, useSpring, HTMLMotionProps } from 'framer-motion';
4+
import { useRef } from 'react';
5+
6+
interface ButtonProps extends HTMLMotionProps<"button"> {
47
children: React.ReactNode;
5-
onClick?: () => void;
68
variant?: 'primary' | 'secondary' | 'danger';
7-
disabled?: boolean;
8-
type?: 'button' | 'submit';
9-
className?: string;
109
}
1110

12-
import { motion } from 'framer-motion';
13-
1411
export function Button({
1512
children,
16-
onClick,
1713
variant = 'primary',
18-
disabled = false,
19-
type = 'button',
2014
className = '',
15+
...props
2116
}: ButtonProps) {
22-
// Base classes provided by globals.css .cyber-button
23-
// We can add variant-specific overrides if needed, but the main style is consistent
17+
const ref = useRef<HTMLButtonElement>(null);
18+
19+
const x = useMotionValue(0);
20+
const y = useMotionValue(0);
21+
22+
// Smooth springs for magnetic effect - weak strength
23+
const springConfig = { damping: 15, stiffness: 150, mass: 0.1 };
24+
const springX = useSpring(x, springConfig);
25+
const springY = useSpring(y, springConfig);
26+
27+
/* Cache rect on mouse enter to avoid layout thrashing */
28+
const rectRef = useRef<DOMRect | null>(null);
29+
30+
const handleMouseEnter = (e: React.MouseEvent<HTMLButtonElement>) => {
31+
rectRef.current = e.currentTarget.getBoundingClientRect();
32+
};
33+
34+
const handleMouseMove = (e: React.MouseEvent<HTMLButtonElement>) => {
35+
if (props.disabled) return;
36+
const { clientX, clientY } = e;
37+
38+
// Use cached rect if available, fallback to getBoundingClientRect if not
39+
const rect = rectRef.current || e.currentTarget.getBoundingClientRect();
40+
const { left, top, width, height } = rect;
41+
const centerX = left + width / 2;
42+
const centerY = top + height / 2;
43+
44+
// Weak magnetic pull
45+
// Divide distance by 8 to limit movement max to ~10px typically
46+
const distanceX = clientX - centerX;
47+
const distanceY = clientY - centerY;
48+
49+
x.set(distanceX / 8);
50+
y.set(distanceY / 8);
51+
};
52+
53+
const handleMouseLeave = () => {
54+
x.set(0);
55+
y.set(0);
56+
};
2457

2558
const variantStyles = {
26-
primary: '', // Standard cyber-button
27-
secondary: 'bg-transparent border-neon-green/30 hover:bg-neon-green/10', // Override for secondary
59+
primary: '',
60+
secondary: 'bg-transparent border-neon-green/30 hover:bg-neon-green/10',
2861
danger: 'border-red-500 text-red-500 hover:bg-red-500/10 hover:text-red-500 hover:shadow-[0_0_20px_rgba(239,68,68,0.4)]',
2962
};
3063

3164
return (
3265
<motion.button
33-
type={type}
34-
onClick={onClick}
35-
disabled={disabled}
36-
whileHover={!disabled ? { scale: 1.02 } : {}}
37-
whileTap={!disabled ? { scale: 0.98 } : {}}
66+
ref={ref}
67+
onMouseMove={handleMouseMove}
68+
onMouseEnter={handleMouseEnter}
69+
onMouseLeave={handleMouseLeave}
70+
style={{ x: springX, y: springY }}
71+
whileHover={!props.disabled ? {
72+
scale: 1.02,
73+
boxShadow: '0 0 15px rgba(0, 255, 65, 0.3)'
74+
} : {}}
75+
whileTap={!props.disabled ? { scale: 0.98 } : {}}
3876
className={`cyber-button ${variantStyles[variant]} ${className}`}
77+
{...props}
3978
>
4079
{children}
4180
</motion.button>

app/components/Card.tsx

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,61 @@
1-
'use client';
2-
3-
import { motion } from 'framer-motion';
1+
import { motion, useMotionTemplate, useMotionValue } from 'framer-motion';
2+
import React, { MouseEvent } from 'react';
43

54
interface CardProps {
65
children: React.ReactNode;
76
className?: string;
87
title?: string;
98
}
109

11-
export function Card({ children, className = '', title }: CardProps) {
10+
export function Card({ children, className = '', title }: Readonly<CardProps>) {
11+
const mouseX = useMotionValue(0);
12+
const mouseY = useMotionValue(0);
13+
14+
/* Cache rect on mouse enter to avoid layout thrashing */
15+
// Using a ref to store the rect
16+
const rectRef = React.useRef<DOMRect | null>(null);
17+
18+
function handleMouseEnter({ currentTarget }: MouseEvent) {
19+
rectRef.current = currentTarget.getBoundingClientRect();
20+
}
21+
22+
function handleMouseMove({ currentTarget, clientX, clientY }: MouseEvent) {
23+
const rect = rectRef.current || currentTarget.getBoundingClientRect();
24+
const { left, top } = rect;
25+
mouseX.set(clientX - left);
26+
mouseY.set(clientY - top);
27+
}
28+
1229
return (
1330
<motion.div
1431
initial={{ opacity: 0, y: 20 }}
1532
animate={{ opacity: 1, y: 0 }}
33+
whileHover={{ y: -4, transition: { duration: 0.2 } }}
1634
transition={{ duration: 0.5 }}
17-
className={`cyber-card p-6 ${className}`}
35+
onMouseMove={handleMouseMove}
36+
onMouseEnter={handleMouseEnter}
37+
className={`cyber-card p-6 group relative ${className}`}
1838
>
19-
{title && (
20-
<div className="mb-4 border-b border-neon-green/20 pb-2">
21-
<h3 className="text-neon-green font-mono font-bold uppercase tracking-wider">{title}</h3>
22-
</div>
23-
)}
24-
{children}
39+
<motion.div
40+
className="pointer-events-none absolute -inset-px rounded-xl opacity-0 transition duration-300 group-hover:opacity-100"
41+
style={{
42+
background: useMotionTemplate`
43+
radial-gradient(
44+
650px circle at ${mouseX}px ${mouseY}px,
45+
rgba(0, 255, 65, 0.15),
46+
transparent 80%
47+
)
48+
`
49+
}}
50+
/>
51+
<div className="relative h-full">
52+
{title && (
53+
<div className="mb-4 border-b border-neon-green/20 pb-2">
54+
<h3 className="text-neon-green font-mono font-bold uppercase tracking-wider">{title}</h3>
55+
</div>
56+
)}
57+
{children}
58+
</div>
2559
</motion.div>
2660
);
2761
}

0 commit comments

Comments
 (0)