Native Magnetic

PreviousNext

Magnetic wrapper that applies a cursor-following effect to any content.

Docs
uitripledcomponent

Preview

Loading preview…
components/native/carbon/native-magnetic-carbon.tsx
"use client";

import { cn } from "@/lib/utils";
import {
  motion,
  Transition,
  useMotionValue,
  useSpring,
  useTransform,
} from "framer-motion";
import { useRef, useState } from "react";

export interface NativeMagneticProps {
  /**
   * Content to apply the magnetic effect to.
   */
  children: React.ReactNode;
  /**
   * Strength of the magnetic pull (0-1 range recommended).
   * Default: 0.3
   */
  strength?: number;
  /**
   * Spring stiffness for the animation.
   * Default: 300
   */
  stiffness?: number;
  /**
   * Spring damping for the animation.
   * Default: 30
   */
  damping?: number;
  /**
   * Whether to scale up on hover.
   * Default: true
   */
  scaleOnHover?: boolean;
  /**
   * Wrapper element type.
   * Default: 'div'
   */
  as?: "div" | "button" | "a";
  /**
   * Link href (only applies when as="a").
   */
  href?: string;
  /**
   * Click handler.
   */
  onClick?: () => void;
  className?: string;
}

const springTransition: Transition = {
  type: "spring",
  stiffness: 400,
  damping: 25,
};

export function NativeMagnetic({
  children,
  strength = 0.3,
  stiffness = 300,
  damping = 30,
  scaleOnHover = true,
  as = "div",
  href,
  onClick,
  className,
}: NativeMagneticProps) {
  const ref = useRef<HTMLElement>(null);
  const [, setIsHovered] = useState(false);

  const x = useMotionValue(0);
  const y = useMotionValue(0);

  const springConfig = { stiffness, damping };
  const springX = useSpring(x, springConfig);
  const springY = useSpring(y, springConfig);

  const translateX = useTransform(springX, (v) => v * strength);
  const translateY = useTransform(springY, (v) => v * strength);

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

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

    x.set(e.clientX - centerX);
    y.set(e.clientY - centerY);
  };

  const handleMouseLeave = () => {
    x.set(0);
    y.set(0);
    setIsHovered(false);
  };

  const commonProps = {
    onMouseMove: handleMouseMove,
    onMouseEnter: () => setIsHovered(true),
    onMouseLeave: handleMouseLeave,
    style: {
      x: translateX,
      y: translateY,
    },
    whileHover: scaleOnHover ? { scale: 1.05 } : undefined,
    whileTap: { scale: 0.95 },
    transition: springTransition,
    className: cn("inline-block cursor-pointer", className),
    onClick,
  };

  if (as === "a" && href) {
    return (
      <motion.a
        ref={ref as React.RefObject<HTMLAnchorElement>}
        href={href}
        {...commonProps}
      >
        {children}
      </motion.a>
    );
  }

  if (as === "button") {
    return (
      <motion.button
        ref={ref as React.RefObject<HTMLButtonElement>}
        type="button"
        {...commonProps}
      >
        {children}
      </motion.button>
    );
  }

  return (
    <motion.div ref={ref as React.RefObject<HTMLDivElement>} {...commonProps}>
      {children}
    </motion.div>
  );
}

Installation

npx shadcn@latest add @uitripled/native-magnetic

Usage

import { NativeMagnetic } from "@/components/native-magnetic"
<NativeMagnetic />