Floating Card

Preview

Hover over me

Code

"use client";

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

interface FloatingCardProps {
  children: ReactNode;
  className?: string;
  rotationStrength?: number;
  contentTranslateZ?: number;
  glowColor?: string;
  noiseOpacity?: number;
  glassmorphism?: boolean;
  animatedGlow?: boolean;
}

const FloatingCard: FC<FloatingCardProps> = ({
  children,
  className,
  rotationStrength = 25,
  contentTranslateZ = 20,
  glowColor = "radial-gradient(circle at center, #8B5CF6, #EC4899, #F59E0B, #10B981, #8B5CF6)",
  noiseOpacity = 0.1,
  glassmorphism = true,
  animatedGlow = true,
}) => {
  const ref = useRef<HTMLDivElement>(null);

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

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

    const { left, top, width, height } = ref.current.getBoundingClientRect();
    const x = event.clientX - left - width / 2;
    const y = event.clientY - top - height / 2;

    mouseX.set(x);
    mouseY.set(y);
    glowOpacity.set(1);
  };

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

  const rotateX = useTransform(mouseY, [-150, 150], [rotationStrength, -rotationStrength]);
  const rotateY = useTransform(mouseX, [-150, 150], [-rotationStrength, rotationStrength]);

  const springConfig = { stiffness: 150, damping: 20, mass: 0.5 };
  const rotateXSpring = useSpring(rotateX, springConfig);
  const rotateYSpring = useSpring(rotateY, springConfig);
  const glowOpacitySpring = useSpring(glowOpacity, { stiffness: 200, damping: 30 });

  const glowStyle = {
    ...(glowColor?.includes("gradient")
      ? { 
          backgroundImage: glowColor,
          backgroundSize: animatedGlow ? "200% 200%" : "100% 100%",
        }
      : { backgroundColor: glowColor }),
  };

  const noiseBackground = `url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='1' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E")`;

  return (
    <motion.div
      ref={ref}
      onMouseMove={handleMouseMove}
      onMouseLeave={handleMouseLeave}
      style={{
        transformStyle: "preserve-3d",
        rotateX: rotateXSpring,
        rotateY: rotateYSpring,
      }}
      className={cn(
        "relative w-72 h-96 rounded-2xl border border-white/10 shadow-2xl shadow-purple-500/20 overflow-hidden",
        glassmorphism
          ? "bg-gradient-to-br from-purple-900/50 to-indigo-900/50 backdrop-blur-lg"
          : "bg-gradient-to-br from-purple-900 to-indigo-900",
        className
      )}
    >
      {/* Noise Overlay */}
      <div
        className="absolute inset-0 w-full h-full pointer-events-none"
        style={{
          backgroundImage: noiseBackground,
          opacity: noiseOpacity,
        }}
      />

      <div className="absolute inset-0 pointer-events-none" style={{ transformStyle: "preserve-3d" }}>
        {/* The Glow */}
        <motion.div
          style={{
            x: mouseX,
            y: mouseY,
            opacity: glowOpacitySpring,
            ...glowStyle,
          }}
          className={cn(
            "absolute top-1/2 left-1/2 h-48 w-48 -translate-x-1/2 -translate-y-1/2 rounded-full blur-3xl",
            animatedGlow && glowColor?.includes("gradient") && "animate-glow-pan"
          )}
        />
      </div>

      <div
        style={{
          transform: "translateZ(var(--content-translate-z, 20px))",
          transformStyle: "preserve-3d",
          '--content-translate-z': `${contentTranslateZ}px`,
        } as React.CSSProperties}
        className="relative inset-0 flex flex-col items-center justify-center p-4 h-full w-full"
      >
        {children}
      </div>
    </motion.div>
  );
};

export default FloatingCard;