Reactive Background Grid

PreviousNext

Background pattern that reacts to mouse movement and click ripples

Docs
uitripledui

Preview

Loading preview…
components/motion-core/reactive-background-grid.tsx
"use client";

import { AnimatePresence, motion } from "framer-motion";
import { MouseEvent, useState } from "react";

type ReactiveBackgroundGridProps = {
  dots?: boolean;
  density?: number;
};

export function ReactiveBackgroundGrid({
  dots = true,
  density = 20,
}: ReactiveBackgroundGridProps) {
  const [ripples, setRipples] = useState<
    Array<{ id: number; x: number; y: number }>
  >([]);
  const [hoveredDot, setHoveredDot] = useState<{ x: number; y: number } | null>(
    null
  );

  const handleClick = (e: MouseEvent<HTMLDivElement>) => {
    const rect = e.currentTarget.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const y = e.clientY - rect.top;

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

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

  const [containerSize, setContainerSize] = useState({
    width: 400,
    height: 400,
  });

  const handleMouseMove = (e: MouseEvent<HTMLDivElement>) => {
    const rect = e.currentTarget.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const y = e.clientY - rect.top;
    setHoveredDot({ x, y });
    setContainerSize({ width: rect.width, height: rect.height });
  };

  const gridSize = density;
  const dotsArray = Array.from({ length: gridSize * gridSize });

  return (
    <div
      onClick={handleClick}
      onMouseMove={handleMouseMove}
      onMouseLeave={() => setHoveredDot(null)}
      className="relative h-96 w-full overflow-hidden rounded-2xl border border-border bg-card"
    >
      <div
        className="absolute inset-0"
        style={{
          backgroundImage: dots
            ? "radial-gradient(circle, currentColor 1px, transparent 1px)"
            : "linear-gradient(to right, currentColor 1px, transparent 1px), linear-gradient(to bottom, currentColor 1px, transparent 1px)",
          backgroundSize: `${100 / gridSize}% ${100 / gridSize}%`,
          backgroundPosition: "0 0",
          color: "rgba(99, 102, 241, 0.1)",
        }}
      />

      {dots &&
        dotsArray.map((_, index) => {
          const row = Math.floor(index / gridSize);
          const col = index % gridSize;
          const x = (col / gridSize) * 100;
          const y = (row / gridSize) * 100;

          const distance = hoveredDot
            ? Math.sqrt(
                Math.pow(x - (hoveredDot.x / containerSize.width) * 100, 2) +
                  Math.pow(y - (hoveredDot.y / containerSize.height) * 100, 2)
              )
            : Infinity;

          const scale = hoveredDot && distance < 30 ? 1.5 : 1;
          const opacity = hoveredDot && distance < 30 ? 0.6 : 0.2;

          return (
            <motion.div
              key={index}
              animate={{
                scale,
                opacity,
              }}
              transition={{
                type: "spring",
                stiffness: 300,
                damping: 25,
              }}
              className="absolute rounded-full bg-primary"
              style={{
                left: `${x}%`,
                top: `${y}%`,
                width: "4px",
                height: "4px",
                transform: "translate(-50%, -50%)",
              }}
            />
          );
        })}

      <AnimatePresence>
        {ripples.map((ripple) => (
          <motion.div
            key={ripple.id}
            initial={{ scale: 0, opacity: 0.8 }}
            animate={{ scale: 4, opacity: 0 }}
            exit={{ scale: 4, opacity: 0 }}
            transition={{ duration: 0.6 }}
            className="absolute rounded-full border-2 border-primary"
            style={{
              left: ripple.x,
              top: ripple.y,
              width: "100px",
              height: "100px",
              transform: "translate(-50%, -50%)",
            }}
          />
        ))}
      </AnimatePresence>
    </div>
  );
}

Installation

npx shadcn@latest add @uitripled/reactive-background-grid

Usage

import { ReactiveBackgroundGrid } from "@/components/ui/reactive-background-grid"
<ReactiveBackgroundGrid />