Back to library

backgrounds

ASMR Background

A dense glass-shard particle field with an interactive magnetic vortex and friction glow that follows the pointer.

Inspired by ashishrajwaniai01 · View source

Preview

Atmospheric Friction

Interactive kinetic environment

Copy for AI

Copy for Lovable is the fastest way to move this component into another Lovable project — it copies the full prompt with dependencies, files, and a usage example. Copy all files gives you the raw source if you want to paste it manually.

Installation

npx shadcn@latest add "https://21st.dev/r/ashishrajwaniai01/asmr-background"

Paste into a terminal — or paste into a Lovable chat and I'll run it for you.

Source files

components/ui/asmr-background.tsx
import { useEffect, useRef, type ReactNode } from "react";
import { cn } from "@/lib/utils";

type AsmrBackgroundProps = {
  children?: ReactNode;
  className?: string;
  particleCount?: number;
};

export function AsmrBackground({
  children,
  className,
  particleCount = 1000,
}: AsmrBackgroundProps) {
  const canvasRef = useRef<HTMLCanvasElement>(null);

  useEffect(() => {
    const canvas = canvasRef.current;
    const container = canvas?.parentElement;
    if (!canvas || !container) return;

    const context = canvas.getContext("2d");
    if (!context) return;
    const ctx = context;

    let width = 0;
    let height = 0;
    let frame = 0;
    let particles: Particle[] = [];
    const pointer = { x: -1000, y: -1000 };
    const magneticRadius = 280;
    const vortexStrength = 0.07;
    const pullStrength = 0.12;
    const reduceMotion = window.matchMedia(
      "(prefers-reduced-motion: reduce)",
    ).matches;

    class Particle {
      x = 0;
      y = 0;
      vx = 0;
      vy = 0;
      size = 0;
      alpha = 0;
      color = "";
      rotation = 0;
      rotationSpeed = 0;
      frictionGlow = 0;

      constructor() {
        this.reset();
      }

      reset() {
        this.x = Math.random() * width;
        this.y = Math.random() * height;
        this.size = Math.random() * 1.5 + 0.5;
        this.vx = (Math.random() - 0.5) * 0.2;
        this.vy = (Math.random() - 0.5) * 0.2;
        this.color = Math.random() > 0.7 ? "240, 245, 255" : "80, 80, 85";
        this.alpha = Math.random() * 0.4 + 0.1;
        this.rotation = Math.random() * Math.PI * 2;
        this.rotationSpeed = (Math.random() - 0.5) * 0.05;
      }

      update() {
        const dx = pointer.x - this.x;
        const dy = pointer.y - this.y;
        const distance = Math.max(Math.hypot(dx, dy), 0.01);

        if (distance < magneticRadius) {
          const force = (magneticRadius - distance) / magneticRadius;
          this.vx += (dx / distance) * force * pullStrength;
          this.vy += (dy / distance) * force * pullStrength;
          this.vx += (dy / distance) * force * vortexStrength * 10;
          this.vy -= (dx / distance) * force * vortexStrength * 10;
          this.frictionGlow = force * 0.7;
        } else {
          this.frictionGlow *= 0.92;
        }

        this.x += this.vx;
        this.y += this.vy;
        this.vx = this.vx * 0.95 + (Math.random() - 0.5) * 0.04;
        this.vy = this.vy * 0.95 + (Math.random() - 0.5) * 0.04;
        this.rotation +=
          this.rotationSpeed + (Math.abs(this.vx) + Math.abs(this.vy)) * 0.05;

        if (this.x < -20) this.x = width + 20;
        if (this.x > width + 20) this.x = -20;
        if (this.y < -20) this.y = height + 20;
        if (this.y > height + 20) this.y = -20;
      }

      draw() {
        ctx.save();
        ctx.translate(this.x, this.y);
        ctx.rotate(this.rotation);
        ctx.fillStyle = `rgba(${this.color}, ${Math.min(this.alpha + this.frictionGlow, 0.9)})`;

        if (this.frictionGlow > 0.3) {
          ctx.shadowBlur = 8 * this.frictionGlow;
          ctx.shadowColor = `rgba(180, 220, 255, ${this.frictionGlow})`;
        }

        ctx.beginPath();
        ctx.moveTo(0, -this.size * 2.5);
        ctx.lineTo(this.size, 0);
        ctx.lineTo(0, this.size * 2.5);
        ctx.lineTo(-this.size, 0);
        ctx.closePath();
        ctx.fill();
        ctx.restore();
      }
    }

    const resize = () => {
      const rect = container.getBoundingClientRect();
      const pixelRatio = Math.min(window.devicePixelRatio || 1, 2);
      width = rect.width;
      height = rect.height;
      canvas.width = Math.max(1, Math.round(width * pixelRatio));
      canvas.height = Math.max(1, Math.round(height * pixelRatio));
      canvas.style.width = `${width}px`;
      canvas.style.height = `${height}px`;
      ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
      particles = Array.from({ length: particleCount }, () => new Particle());
    };

    const draw = () => {
      ctx.fillStyle = "rgba(10, 10, 12, 0.18)";
      ctx.fillRect(0, 0, width, height);
      particles.forEach((particle) => {
        if (!reduceMotion) particle.update();
        particle.draw();
      });
      if (!reduceMotion) frame = requestAnimationFrame(draw);
    };

    const updatePointer = (event: PointerEvent) => {
      const rect = container.getBoundingClientRect();
      pointer.x = event.clientX - rect.left;
      pointer.y = event.clientY - rect.top;
    };
    const clearPointer = () => {
      pointer.x = -1000;
      pointer.y = -1000;
    };

    const observer = new ResizeObserver(resize);
    observer.observe(container);
    container.addEventListener("pointermove", updatePointer);
    container.addEventListener("pointerleave", clearPointer);
    resize();
    draw();

    return () => {
      observer.disconnect();
      container.removeEventListener("pointermove", updatePointer);
      container.removeEventListener("pointerleave", clearPointer);
      cancelAnimationFrame(frame);
    };
  }, [particleCount]);

  return (
    <div
      className={cn(
        "relative isolate h-full w-full overflow-hidden bg-neutral-950",
        className,
      )}
    >
      <canvas ref={canvasRef} className="absolute inset-0 block" />
      {children ? <div className="relative z-10 h-full">{children}</div> : null}
    </div>
  );
}

export default AsmrBackground;