Micro-Interaction Loaders

Premium status indicators with GSAP-powered physics.

Component Based
01

Magnifying Glass

npx shadcn@latest add https://aftershade.pages.dev/registry/magnifying-glass-loader.json
"use client";
import React, { useRef, useEffect } from "react";
import gsap from "gsap";
import { cn } from "@/lib/utils";

interface MagnifyingGlassLoaderProps {
    className?: string;
    color?: string;
}

export function MagnifyingGlassLoader({
    className,
    color = "currentColor"
}: MagnifyingGlassLoaderProps) {
    const glassRef = useRef<SVGSVGElement>(null);
    const lensRef = useRef<SVGCircleElement>(null);
    const handleRef = useRef<SVGPathElement>(null);
    const shineRef = useRef<SVGPathElement>(null);

    useEffect(() => {
        if (!glassRef.current || !lensRef.current || !handleRef.current || !shineRef.current) return;
        const tl = gsap.timeline({ repeat: -1 });

        gsap.set(glassRef.current, { transformOrigin: "center center" });
        gsap.set([lensRef.current, handleRef.current], { transformOrigin: "center center" });

        tl.to(glassRef.current, { rotation: -15, x: -15, y: -5, duration: 0.8, ease: "power2.inOut" })
          .to(glassRef.current, { rotation: 10, x: 15, y: 5, duration: 1.2, ease: "power2.inOut" })
          .to(glassRef.current, { rotation: 0, x: 0, y: 0, duration: 0.8, ease: "elastic.out(1, 0.5)" });

        gsap.to(lensRef.current, { scale: 1.1, strokeWidth: 2.5, duration: 0.6, repeat: -1, yoyo: true, ease: "sine.inOut" });
        
        const shineTl = gsap.timeline({ repeat: -1, repeatDelay: 1.5 });
        shineTl.fromTo(shineRef.current, { x: -10, opacity: 0, scale: 0.5 }, { x: 10, opacity: 1, scale: 1.2, duration: 0.6, ease: "power2.out" })
               .to(shineRef.current, { opacity: 0, duration: 0.2 }, "-=0.2");
        return () => { tl.kill(); shineTl.kill(); };
    }, []);

    return (
        <div className={cn("relative w-24 h-24 flex items-center justify-center", className)}>
            <svg ref={glassRef} width="64" height="64" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-primary-1">
                <circle ref={lensRef} cx="11" cy="11" r="8" className="transition-colors" />
                <path ref={handleRef} d="m21 21-4.3-4.3" />
                <path ref={shineRef} d="M8 8a4 4 0 0 1 3-1" stroke="white" strokeWidth="1.5" strokeOpacity="0.8" className="blur-[0.5px]" />
            </svg>
        </div>
    );
}
02

Puzzle Loader

npx shadcn@latest add https://aftershade.pages.dev/registry/puzzle-loader.json
"use client";
import React, { useRef, useEffect } from "react";
import gsap from "gsap";
import { cn } from "@/lib/utils";

export function PuzzleLoader({ className, gap = 4, color = "bg-primary-1" }: any) {
    const containerRef = useRef<HTMLDivElement>(null);
    const boxesRef = useRef<(HTMLDivElement | null)[]>([]);

    useEffect(() => {
        const boxes = boxesRef.current;
        if (boxes.length !== 4) return;
        gsap.set(boxes, { x: 0, y: 0, scale: 1, borderRadius: "20%" });

        const moveDist = 24; 
        const tl = gsap.timeline({ repeat: -1, repeatDelay: 0.2 });

        tl.to(boxes, { scale: 0.85, borderRadius: "40%", duration: 0.3, ease: "back.in(2)" });
        tl.to(boxes[0], { x: moveDist, duration: 0.5, ease: "power4.inOut" }, "swap1")
          .to(boxes[1], { y: moveDist, duration: 0.5, ease: "power4.inOut" }, "swap1")
          .to(boxes[3], { x: -moveDist, duration: 0.5, ease: "power4.inOut" }, "swap1")
          .to(boxes[2], { y: -moveDist, duration: 0.5, ease: "power4.inOut" }, "swap1");
        tl.to(boxes, { scale: 1, borderRadius: "20%", duration: 0.3, ease: "back.out(2)" });
        tl.to(containerRef.current, { rotation: 90, duration: 0.6, ease: "expo.inOut", delay: 0.2 });

        return () => { tl.kill(); };
    }, []);

    return (
        <div className={cn("w-24 h-24 flex items-center justify-center", className)}>
            <div ref={containerRef} className="relative w-12 h-12">
                <div className="absolute top-0 left-0 grid grid-cols-2 gap-1 w-full h-full">
                    {[0, 1, 2, 3].map((i) => (
                        <div key={i} ref={(el) => { boxesRef.current[i] = el; }} className={cn("w-5 h-5 rounded-md", color)} />
                    ))}
                </div>
            </div>
        </div>
    );
}
03

Progress Bar

npx shadcn@latest add https://aftershade.pages.dev/registry/progress-loader.json
"use client";
import React, { useRef, useEffect } from "react";
import gsap from "gsap";
import { cn } from "@/lib/utils";

interface ProgressLoaderProps {
    className?: string;
    width?: number | string;
    height?: number | string;
    color?: string;
    trackColor?: string;
}

export function ProgressLoader({
    className,
    width = "100%",
    height = 4,
    color = "bg-primary-1",
    trackColor = "bg-secondary"
}: ProgressLoaderProps) {
    const barRef = useRef<HTMLDivElement>(null);
    const glowRef = useRef<HTMLDivElement>(null);

    useEffect(() => {
        if (!barRef.current || !glowRef.current) return;
        const tl = gsap.timeline({ repeat: -1 });

        gsap.set(barRef.current, { xPercent: -100, scaleX: 0.2, transformOrigin: "left center" });
        gsap.set(glowRef.current, { opacity: 0 });

        tl.to(barRef.current, { xPercent: -20, scaleX: 0.6, duration: 1, ease: "power3.inOut" })
          .to(glowRef.current, { opacity: 0.8, duration: 0.3 }, "<");
        tl.to(barRef.current, { xPercent: 20, scaleX: 0.8, duration: 1.2, ease: "linear" });
        tl.to(barRef.current, { xPercent: 105, scaleX: 0.1, duration: 0.8, ease: "power4.out" })
          .to(glowRef.current, { opacity: 0, duration: 0.4 }, "-=0.2");

        return () => { tl.kill(); };
    }, []);

    return (
        <div className={cn("w-full max-w-xs", className)}>
            <div className={cn("relative overflow-hidden rounded-full", trackColor)} style={{ height, width }}>
                <div ref={barRef} className={cn("absolute top-0 left-0 h-full w-full rounded-full origin-left", color)}>
                    <div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/30 to-transparent w-full h-full opacity-50" />
                </div>
            </div>
        </div>
    );
}
04

Multi-Ring Spinner

npx shadcn@latest add https://aftershade.pages.dev/registry/spinner-loader.json
"use client";
import React, { useRef, useEffect } from "react";
import gsap from "gsap";
import { cn } from "@/lib/utils";

export function SpinnerLoader({ className, size = 64, color = "currentColor" }: any) {
    const svgRef = useRef<SVGSVGElement>(null);
    const ring1Ref = useRef<SVGCircleElement>(null);
    const ring2Ref = useRef<SVGCircleElement>(null);
    const ring3Ref = useRef<SVGCircleElement>(null);

    useEffect(() => {
        if (!ring1Ref.current) return;
        const rings = [ring1Ref.current, ring2Ref.current, ring3Ref.current];
        gsap.set(rings, { transformOrigin: "center center" });
        
        gsap.to(svgRef.current, { rotation: 360, duration: 20, repeat: -1, ease: "none" });
        gsap.to(ring1Ref.current, { rotation: 360, duration: 1.5, repeat: -1, ease: "power1.inOut" });
        gsap.to(ring2Ref.current, { rotation: 360, duration: 2, repeat: -1, ease: "elastic.inOut(1.2, 0.7)", delay: 0.1 });
        gsap.to(ring3Ref.current, { rotation: -360, duration: 3, repeat: -1, ease: "sine.inOut" });
        gsap.to(rings, { strokeWidth: 4, duration: 1, yoyo: true, repeat: -1, ease: "sine.inOut", stagger: 0.2 });
    }, []);

    return (
        <div className={cn("relative flex items-center justify-center", className)} style={{ width: size, height: size }}>
            <svg ref={svgRef} viewBox="0 0 100 100" fill="none" className="w-full h-full text-primary-1">
                <circle ref={ring3Ref} cx="50" cy="50" r="40" stroke={color} strokeWidth="1" strokeOpacity="0.3" strokeDasharray="10 5" strokeLinecap="round" />
                <circle ref={ring2Ref} cx="50" cy="50" r="32" stroke={color} strokeWidth="2" strokeOpacity="0.6" strokeDasharray="50 150" strokeLinecap="round" />
                <circle ref={ring1Ref} cx="50" cy="50" r="24" stroke={color} strokeWidth="3" strokeDasharray="100 200" strokeLinecap="round" className="drop-shadow-[0_0_8px_rgba(255,255,255,0.5)]" />
            </svg>
        </div>
    );
}
05

Orbital Dots

npx shadcn@latest add https://aftershade.pages.dev/registry/orbital-dots-loader.json
"use client";
import React, { useRef, useEffect } from "react";
import gsap from "gsap";
import { cn } from "@/lib/utils";

export function OrbitalDotsLoader({ className, size = 40, color = "bg-primary-1" }: any) {
    const dot1Ref = useRef<HTMLDivElement>(null);
    const dot2Ref = useRef<HTMLDivElement>(null);
    const centerRef = useRef<HTMLDivElement>(null);

    useEffect(() => {
        if (!dot1Ref.current) return;
        gsap.to(dot1Ref.current, { rotation: 360, duration: 1.5, repeat: -1, ease: "linear", transformOrigin: "20px 20px" });
        gsap.to(dot2Ref.current, { rotation: -360, duration: 2.5, repeat: -1, ease: "linear", transformOrigin: "-10px -10px" });
        gsap.to(centerRef.current, { scale: 1.5, opacity: 0.5, duration: 0.8, yoyo: true, repeat: -1, ease: "sine.inOut" });
    }, []);

    return (
        <div className={cn("relative flex items-center justify-center", className)} style={{ width: size * 3, height: size * 3 }}>
            <div ref={centerRef} className={cn("w-3 h-3 rounded-full absolute", color)} />
            <div ref={dot1Ref} className="absolute w-full h-full flex items-center justify-center"><div className={cn("w-3 h-3 rounded-full absolute -top-4", color)} /></div>
            <div ref={dot2Ref} className="absolute w-full h-full flex items-center justify-center"><div className={cn("w-2 h-2 rounded-full absolute -bottom-8 opacity-60", color)} /></div>
        </div>
    );
}
06

Wave Bar

npx shadcn@latest add https://aftershade.pages.dev/registry/wave-bar-loader.json
"use client";
import React, { useRef, useEffect } from "react";
import gsap from "gsap";
import { cn } from "@/lib/utils";

export function WaveBarLoader({ className, count = 5, color = "bg-primary-1" }: any) {
    const barsRef = useRef<(HTMLDivElement | null)[]>([]);

    useEffect(() => {
        if (barsRef.current.length === 0) return;
        gsap.to(barsRef.current, { scaleY: 2.5, duration: 0.4, repeat: -1, yoyo: true, ease: "sine.inOut", stagger: { each: 0.1, from: "center" } });
    }, []);

    return (
        <div className={cn("flex items-center gap-1 h-12", className)}>
            {Array.from({ length: count }).map((_, i) => (
                <div key={i} ref={(el) => { barsRef.current[i] = el; }} className={cn("w-1.5 h-4 rounded-full", color)} />
            ))}
        </div>
    );
}
07

Grid Pulse

npx shadcn@latest add https://aftershade.pages.dev/registry/grid-pulse-loader.json
"use client";
import React, { useRef, useEffect } from "react";
import gsap from "gsap";
import { cn } from "@/lib/utils";

export function GridPulseLoader({ className, color = "bg-primary-1" }: any) {
    const dotsRef = useRef<(HTMLDivElement | null)[]>([]);

    useEffect(() => {
        gsap.to(dotsRef.current, { opacity: 0.2, scale: 0.5, duration: 0.8, repeat: -1, yoyo: true, ease: "power1.inOut", stagger: { grid: [3, 3], from: "random", amount: 1 } });
    }, []);

    return (
        <div className={cn("grid grid-cols-3 gap-1", className)}>
            {Array.from({ length: 9 }).map((_, i) => (
                <div key={i} ref={(el) => { dotsRef.current[i] = el; }} className={cn("w-3 h-3 rounded-sm", color)} />
            ))}
        </div>
    );
}
08

Quantum Core

npx shadcn@latest add https://aftershade.pages.dev/registry/quantum-ring-loader.json
"use client";
import React, { useRef, useEffect } from "react";
import gsap from "gsap";
import { cn } from "@/lib/utils";

export function QuantumRingLoader({ className, size = 200, color = "text-primary-1" }: any) {
    const ring1Ref = useRef<SVGCircleElement>(null);
    const ring2Ref = useRef<SVGCircleElement>(null);
    const coreRef = useRef<SVGCircleElement>(null);

    useEffect(() => {
        if (!ring1Ref.current) return;
        gsap.to(ring1Ref.current, { rotation: 360, transformOrigin: "center center", duration: 2, repeat: -1, ease: "linear" });
        gsap.to(ring2Ref.current, { rotation: -360, transformOrigin: "center center", duration: 2.5, repeat: -1, ease: "linear" });
        gsap.to([ring1Ref.current, ring2Ref.current], { scale: 1.1, transformOrigin: "center center", strokeWidth: 1.5, duration: 1, repeat: -1, yoyo: true, ease: "sine.inOut" });
        gsap.to(coreRef.current, { scale: 1.5, opacity: 0.8, duration: 0.5, repeat: -1, yoyo: true, ease: "power2.inOut" });
    }, []);

    return (
        <div className={cn("relative flex items-center justify-center", className)} style={{ width: 64, height: 64 }}>
            <svg width="64" height="64" viewBox="0 0 100 100" fill="none" className={color}>
                <circle ref={coreRef} cx="50" cy="50" r="8" fill="currentColor" className="opacity-50 blur-[2px]" />
                <circle ref={ring1Ref} cx="50" cy="50" r="30" stroke="currentColor" strokeWidth="2" fill="none" className="opacity-80" style={{ transformBox: "fill-box" }} />
                <circle ref={ring2Ref} cx="50" cy="50" r="30" stroke="currentColor" strokeWidth="2" fill="none" className="opacity-60" style={{ transform: "rotate(90deg)", transformBox: "fill-box" }} />
            </svg>
        </div>
    );
}
09

Code Stream

npx shadcn@latest add https://aftershade.pages.dev/registry/code-typer-loader.json
"use client";
import React, { useRef, useEffect } from "react";
import gsap from "gsap";
import { cn } from "@/lib/utils";

export function CodeTyperLoader({ className, color = "bg-primary-1" }: any) {
    const linesRef = useRef<(HTMLDivElement | null)[]>([]);

    useEffect(() => {
        if (linesRef.current.length === 0) return;
        const tl = gsap.timeline({ repeat: -1, repeatDelay: 0.5 });
        tl.to(linesRef.current, { width: (i) => ["60%", "80%", "40%", "90%"][i], duration: 0.4, stagger: 0.1, ease: "steps(4)" });
        tl.to(linesRef.current, { opacity: 0.5, duration: 0.2, yoyo: true, repeat: 1, ease: "power1.inOut" });
        tl.to(linesRef.current, { width: 0, duration: 0.2, stagger: 0.05, ease: "power2.in" });
    }, []);

    return (
        <div className={cn("flex flex-col gap-2 w-16", className)}>
            {Array.from({ length: 4 }).map((_, i) => (
                <div key={i} ref={(el) => { linesRef.current[i] = el; }} className={cn("h-2 rounded-full w-0", color)} />
            ))}
        </div>
    );
}
10

Liquid Goo

npx shadcn@latest add https://aftershade.pages.dev/registry/liquid-blob-loader.json
"use client";
import React, { useRef, useEffect } from "react";
import gsap from "gsap";
import { cn } from "@/lib/utils";

export function LiquidBlobLoader({ className, color = "bg-primary-1" }: any) {
    const blob1Ref = useRef<HTMLDivElement>(null);
    const blob2Ref = useRef<HTMLDivElement>(null);
    const filterId = React.useId();

    useEffect(() => {
        if (!blob1Ref.current) return;
        const tl = gsap.timeline({ repeat: -1, yoyo: true });
        tl.to(blob1Ref.current, { x: -15, scale: 0.8, duration: 1.2, ease: "power2.inOut" }, 0);
        tl.to(blob2Ref.current, { x: 15, scale: 0.8, duration: 1.2, ease: "power2.inOut" }, 0);
        gsap.to([blob1Ref.current, blob2Ref.current], { rotation: 180, duration: 2.4, repeat: -1, yoyo: true, ease: "sine.inOut" });
    }, []);

    return (
        <div className={cn("relative w-24 h-24 flex items-center justify-center", className)}>
            <svg style={{ position: 'absolute', width: 0, height: 0 }}>
                <filter id={`goo-${filterId}`}>
                    <feGaussianBlur in="SourceGraphic" stdDeviation="10" result="blur" />
                    <feColorMatrix in="blur" mode="matrix" values="1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 18 -7" result="goo" />
                    <feComposite in="SourceGraphic" in2="goo" operator="atop" />
                </filter>
            </svg>
            <div className="relative w-full h-full flex items-center justify-center" style={{ filter: `url(#goo-${filterId})` }}>
                <div ref={blob1Ref} className={cn("absolute w-12 h-12 rounded-full blur-sm", color)} />
                <div ref={blob2Ref} className={cn("absolute w-12 h-12 rounded-full blur-sm", color)} />
            </div>
        </div>
    );
}