SNYTH
Web

Button

Explore the Snyth registry of premium, high-performance UI blocks.

Preview

Installation

1. Install Dependencies

Ensure you have motion installed in your project.

npm install motion
yarn add motion
pnpm add motion
bun add motion

2. Add Component

Using CLI

Use the Snyth CLI to automatically add the button component to your project.

npx snyth add button
bunx snyth add button

Manual Installation

Create the file at components/file-path/button.tsx.

"use client";

import React, { useRef, useState } from "react";
import {
  motion,
  useMotionValue,
  useSpring,
  useTransform,
  AnimatePresence,
} from "motion/react";

interface MagneticButtonProps {
  label?: string;
  onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
  variant?: "primary" | "secondary";
}

interface Ripple {
  id: number;
  x: number;
  y: number;
}

const Button: React.FC<MagneticButtonProps> = ({
  label = "Hover me",
  onClick,
}) => {
  const buttonRef = useRef<HTMLButtonElement>(null);
  const [ripples, setRipples] = useState<Ripple[]>([]);
  const [isHovered, setIsHovered] = useState<boolean>(false);

  const mouseX = useMotionValue(0);
  const mouseY = useMotionValue(0);

  const springConfig = { damping: 25, stiffness: 200, mass: 0.6 };
  const springX = useSpring(mouseX, springConfig);
  const springY = useSpring(mouseY, springConfig);

  const textX = useTransform(springX, (val) => val * 0.4);
  const textY = useTransform(springY, (val) => val * 0.4);

  const glowX = useMotionValue(0);
  const glowY = useMotionValue(0);
  const springGlowX = useSpring(glowX, { damping: 40, stiffness: 300 });
  const springGlowY = useSpring(glowY, { damping: 40, stiffness: 300 });

  const handleMouseMove = (e: React.MouseEvent<HTMLButtonElement>) => {
    if (!buttonRef.current) return;

    const rect = buttonRef.current.getBoundingClientRect();
    const centerX = rect.left + rect.width / 2;
    const centerY = rect.top + rect.height / 2;

    const distanceX = e.clientX - centerX;
    const distanceY = e.clientY - centerY;

    mouseX.set(distanceX * 0.3);
    mouseY.set(distanceY * 0.3);

    glowX.set(e.clientX - rect.left);
    glowY.set(e.clientY - rect.top);
  };

  const handleMouseLeave = () => {
    setIsHovered(false);
    mouseX.set(0);
    mouseY.set(0);
  };

  const handleMouseEnter = () => {
    setIsHovered(true);
  };

  const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
    if (!buttonRef.current) return;

    const rect = buttonRef.current.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const y = e.clientY - rect.top;

    const newRipple: Ripple = {
      id: Date.now(),
      x,
      y,
    };

    setRipples((prev) => [...prev, newRipple]);
    if (onClick) onClick(e);

    setTimeout(() => {
      setRipples((prev) => prev.filter((r) => r.id !== newRipple.id));
    }, 1000);
  };

  const glowBg = useTransform(
    [springGlowX, springGlowY],
    ([x, y]) =>
      `radial-gradient(circle 140px at ${x}px ${y}px, var(--glow-color), transparent)`
  );

  const borderGlowBg = useTransform(
    [springGlowX, springGlowY],
    ([x, y]) =>
      `radial-gradient(circle 100px at ${x}px ${y}px, var(--border-glow), transparent)`
  );

  return (
    <motion.button
      ref={buttonRef}
      onMouseMove={handleMouseMove}
      onMouseEnter={handleMouseEnter}
      onMouseLeave={handleMouseLeave}
      onClick={handleClick}
      style={{
        x: springX,
        y: springY,
      } as any}
      className={`
        relative group overflow-hidden px-10 py-5 rounded-2xl
        flex items-center justify-center gap-4 select-none
        cursor-none md:cursor-pointer min-w-[240px]
        bg-white border-black/10 text-neutral-900
        [--glow-color:rgba(168,85,247,0.15)]
        [--border-glow:rgba(0,0,0,0.1)]
        dark:bg-neutral-950 dark:border-white/10 dark:text-white
        dark:[--glow-color:rgba(168,85,247,0.2)]
        dark:[--border-glow:rgba(255,255,255,0.15)]
        transition-colors duration-300
      `}
      animate={{
        scale: isHovered ? 1.05 : 1,
        borderRadius: isHovered ? "32px" : "16px",
        borderColor: isHovered
          ? "rgba(168, 85, 247, 0.4)"
          : "transparent",
      }}
      transition={{ type: "spring", damping: 20, stiffness: 200 }}
    >
      <div className="absolute inset-0 opacity-[0.03] dark:opacity-[0.04] pointer-events-none mix-blend-overlay">
        <svg width="100%" height="100%">
          <filter id="grain">
            <feTurbulence
              type="fractalNoise"
              baseFrequency="0.8"
              numOctaves="4"
              stitchTiles="stitch"
            />
          </filter>
          <rect width="100%" height="100%" filter="url(#grain)" />
        </svg>
      </div>

      <motion.div
        className="absolute inset-0 pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity duration-500"
        style={{ background: glowBg }}
      />

      <motion.div
        className="absolute inset-[-1px] rounded-inherit pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity duration-300"
        style={{ background: borderGlowBg }}
      />

      <AnimatePresence>
        {ripples.map((ripple) => (
          <motion.span
            key={ripple.id}
            initial={{ scale: 0, opacity: 0.5 }}
            animate={{ scale: 5, opacity: 0 }}
            exit={{ opacity: 0 }}
            transition={{ duration: 1, ease: "circOut" }}
            className="absolute bg-neutral-400/20 dark:bg-white/10 rounded-full pointer-events-none z-0"
            style={{
              width: 100,
              height: 100,
              left: ripple.x - 50,
              top: ripple.y - 50,
            }}
          />
        ))}
      </AnimatePresence>

      <motion.div
        className="relative z-10 flex items-center justify-center gap-4 w-full"
        style={{ x: textX, y: textY }}
      >
        <span className="font-semibold tracking-widest text-xs uppercase text-center whitespace-nowrap">
          {label}
        </span>
      </motion.div>

      <div className="absolute bottom-0 left-1/2 -translate-x-1/2 w-3/4 h-[1px] bg-gradient-to-r from-transparent via-purple-500/40 to-transparent blur-[1px]" />
    </motion.button>
  );
};

export default Button;

Usage

import Button from "@/components/file-path/button";

export default function Example() {
  return (
    <div className="flex gap-4">
      <Button text="Explore Snyth" />
      <Button text="Get Started" />
    </div>
  );
}