Interactive button components with magnetic effects and smooth animations
Interactive buttons with subtle cursor tracking effect and smooth spring animations.
import { InteractiveButton } from "@/components/ui/interactive-button";
export default function Example() {
return (
<div className="flex gap-4">
<InteractiveButton variant="accent">
Accent
</InteractiveButton>
<InteractiveButton variant="secondary">
Secondary
</InteractiveButton>
<InteractiveButton variant="glass">
Glass
</InteractiveButton>
<InteractiveButton variant="default">
Default
</InteractiveButton>
<InteractiveButton variant="ghost">
Ghost
</InteractiveButton>
</div>
);
}"use client";
import React, { useRef, useState } from "react";
import { motion } from "framer-motion";
import { cn } from "@/app/lib/utils";
interface InteractiveButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
children: React.ReactNode;
variant?: "default" | "accent" | "secondary" | "ghost" | "glass";
size?: "sm" | "md" | "lg";
magneticStrength?: number;
}
export function InteractiveButton({
children,
className,
variant = "default",
size = "md",
magneticStrength = 0.3,
onClick,
disabled,
type,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
onDrag: _onDrag,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
onDragEnd: _onDragEnd,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
onDragStart: _onDragStart,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
onAnimationStart: _onAnimationStart,
...props
}: InteractiveButtonProps) {
const buttonRef = useRef<HTMLButtonElement>(null);
const [position, setPosition] = useState({ x: 0, y: 0 });
const handleMouse = (e: React.MouseEvent<HTMLButtonElement>) => {
const button = buttonRef.current;
if (!button) return;
const rect = button.getBoundingClientRect();
const x =
(e.clientX - rect.left - rect.width / 2) * (magneticStrength * 0.08);
const y =
(e.clientY - rect.top - rect.height / 2) * (magneticStrength * 0.08);
setPosition({ x, y });
};
const handleMouseLeave = () => {
setPosition({ x: 0, y: 0 });
};
const variants = {
default:
"bg-surface border border-border hover:border-border/70 text-foreground backdrop-blur-xl",
accent:
"bg-gradient-to-r from-accent via-accent-secondary to-accent bg-[length:200%_100%] hover:bg-[length:100%_100%] text-primary-foreground border-0 backdrop-blur-xl transition-all",
secondary:
"bg-surface/40 hover:bg-surface/60 text-foreground border border-accent/40 hover:border-accent/60 hover:shadow-lg hover:shadow-accent/10 backdrop-blur-xl transition-all",
ghost:
"bg-transparent hover:bg-surface/30 text-foreground border border-border/40 hover:border-border/60 backdrop-blur-xl transition-all",
glass:
"border-0 text-white overflow-hidden before:content-[''] before:absolute before:inset-0 before:z-0 before:rounded-lg before:shadow-[inset_2px_2px_0px_-2px_rgba(255,255,255,0.7),inset_0_0_3px_1px_rgba(255,255,255,0.7)] before:bg-white/20 hover:before:bg-white/25 after:content-[''] after:absolute after:-z-10 after:inset-0 after:rounded-lg after:[backdrop-filter:blur(0px)] after:[filter:url(#btn-glass)] after:overflow-hidden after:isolate transition-all",
};
const sizes = {
sm: "px-4 py-2 text-sm",
md: "px-6 py-3 text-base",
lg: "px-8 py-4 text-lg",
};
return (
<motion.button
ref={buttonRef}
type={type}
disabled={disabled}
onClick={onClick}
onMouseMove={handleMouse}
onMouseLeave={handleMouseLeave}
animate={{ x: position.x, y: position.y }}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
transition={{ type: "spring", stiffness: 800, damping: 30, mass: 0.2 }}
className={cn(
"relative rounded-lg font-medium transition-all duration-200",
"focus:outline-none focus:ring-2 focus:ring-accent focus:ring-offset-2 focus:ring-offset-background",
"hover:shadow-lg hover:shadow-accent/20",
variants[variant],
sizes[size],
className
)}
style={
variant === "glass"
? {
isolation: "isolate",
}
: undefined
}
{...props}
>
<span className="relative z-10">{children}</span>
{variant === "glass" && (
<span
className="absolute -z-10 inset-0 rounded-lg overflow-hidden"
style={{
backdropFilter: "blur(0px)",
filter: "url(#btn-glass)",
isolation: "isolate",
}}
/>
)}
</motion.button>
);
}| Name | Type | Default | Description |
|---|---|---|---|
| variant | "default" | "accent" | "secondary" | "glass" | "ghost" | "default" | Visual style variant |
| size | "sm" | "md" | "lg" | "md" | Button size |
| magneticStrength | number | 0.3 | Strength of magnetic effect |
| disabled | boolean | false | Disabled state |