Modern Loader

PreviousNext

A modern, dynamic code loader designed for AI-powered coding environments.

Docs
scrollxuicomponent

Preview

Loading preview…
components/ui/modern-loader.tsx
"use client";
import React, { useState, useEffect, useRef, useCallback, useMemo } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { cn } from "@/lib/utils";
import TypeAnimation from "@/components/ui/typeanimation";

interface ModernLoaderProps {
  words?: string[];
}

const ModernLoader: React.FC<ModernLoaderProps> = ({
  words = ["Setting things up...", "Initializing modules...", "Almost ready..."],
}) => {
  const [currentLine, setCurrentLine] = useState(0);
  const [cursorVisible, setCursorVisible] = useState(true);
  const containerRef = useRef<HTMLDivElement>(null);

  const colors = useMemo(
  () => ["bg-gray-500", "bg-teal-500", "bg-blue-500", "bg-gray-600", "bg-pink-500"],
  []
);
  const BUFFER = 20;
  const MAX_LINES = 100;

  const generateLines = useCallback((count = 20) =>
  Array.from({ length: count }, (_, idx) => ({
    id: Date.now() + idx,
    segments: Array.from({ length: Math.floor(Math.random() * 4) + 1 }, () => ({
      width: `${Math.floor(Math.random() * 80) + 50}px`,
      color: colors[Math.floor(Math.random() * colors.length)],
      isCircle: Math.random() > 0.93,
      indent: Math.random() > 0.7 ? 1 : 0,
    })),
  })),
  [colors]
);

  const [lines, setLines] = useState(() => generateLines());

  const getVisibleRange = () => {
    const start = Math.max(0, currentLine - BUFFER);
    const end = Math.min(lines.length, currentLine + BUFFER);
    return { start, end };
  };

  const { start: visibleStart, end: visibleEnd } = getVisibleRange();

  useEffect(() => {
    if (containerRef.current) {
      containerRef.current.scrollTop = containerRef.current.scrollHeight;
    }
  }, [currentLine]);

  useEffect(() => {
    const timer = setTimeout(() => {
      setCurrentLine((prev) => {
        const nextLine = prev + 1;
        if (nextLine >= lines.length - 10) setLines((old) => [...old, ...generateLines(50)]);
        return nextLine;
      });
    }, 200);
    return () => clearTimeout(timer);
  }, [currentLine, lines.length, generateLines]);

  useEffect(() => {
    const interval = setInterval(() => setCursorVisible((prev) => !prev), 530);
    return () => clearInterval(interval);
  }, []);

  useEffect(() => {
    const cleanup = () => {
      if (lines.length > MAX_LINES && currentLine > BUFFER * 2) {
        setLines((oldLines) => {
          const safeIndex = currentLine - BUFFER * 2;
          if (safeIndex > 0) {
            setCurrentLine((prev) => prev - safeIndex);
            return oldLines.slice(safeIndex);
          }
          return oldLines;
        });
      }
    };
    const interval = setInterval(cleanup, 5000);
    return () => clearInterval(interval);
  }, [currentLine, lines.length]);

  const visibleLines = lines.slice(visibleStart, visibleEnd);

  return (
    <div className="w-full max-w-md mx-auto p-8">
      <motion.div
        initial={{ opacity: 0, scale: 0.95 }}
        animate={{ opacity: 1, scale: 1 }}
        transition={{ duration: 0.3 }}
        className="relative bg-background h-[300px] rounded-2xl shadow-2xl overflow-hidden border border-border"
      >
        <div className="px-4 py-3 flex items-center z-10 relative">
          <div className="flex items-center gap-1.5">
            <motion.div className="w-2 xs:w-2.5 sm:w-3 h-2 xs:h-2.5 sm:h-3 rounded-full bg-red-500" />
            <motion.div className="w-2 xs:w-2.5 sm:w-3 h-2 xs:h-2.5 sm:h-3 rounded-full bg-yellow-500" />
            <motion.div className="w-2 xs:w-2.5 sm:w-3 h-2 xs:h-2.5 sm:h-3 rounded-full bg-green-500" />
          </div>

          <motion.div
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            transition={{ delay: 0.2 }}
            className="flex-1 text-center"
          >
            <TypeAnimation
              words={words}
              typingSpeed="slow"
              deletingSpeed="slow"
              pauseDuration={2000}
              className="text-muted-foreground text-xs font-mono"
            />
          </motion.div>
        </div>

        <div ref={containerRef} className="relative px-5 py-4 font-mono text-sm overflow-y-hidden h-[calc(100%-48px)]">
          <div className="space-y-2 relative z-10">
            <AnimatePresence mode="sync">
              {visibleLines.map((line, idx) => {
                const actualIndex = visibleStart + idx;
                if (actualIndex >= currentLine) return null;
                const extraMargin = (idx + 1) % 4 === 0 ? "mt-2" : "";
                const paddingClass = line.segments[0]?.indent ? "pl-4" : "";
                return (
                  <React.Fragment key={line.id}>
                    <motion.div
                      className={cn("flex items-center gap-2 h-5", extraMargin, paddingClass)}
                      initial={{ opacity: 0, x: -5 }}
                      animate={{ opacity: 1, x: 0 }}
                      exit={{ opacity: 0 }}
                      transition={{ duration: 0.2, ease: "easeOut" }}
                    >
                      {line.segments.map((seg, i) =>
                        seg.isCircle ? (
                          <motion.div
                            key={i}
                            initial={{ scale: 0 }}
                            animate={{ scale: 1 }}
                            transition={{ duration: 0.2, delay: 0.05 }}
                            className={cn("w-4 h-4 rounded-full opacity-50", seg.color)}
                          />
                        ) : (
                          <motion.div
                            key={i}
                            initial={{ width: 0 }}
                            animate={{ width: seg.width }}
                            transition={{ duration: 0.25, ease: "easeOut" }}
                            className={cn("h-3 rounded-sm opacity-50", seg.color)}
                            style={{ width: seg.width }}
                          />
                        )
                      )}
                    </motion.div>

                    {(actualIndex + 1) % 6 === 0 && (
                      <motion.div
                        className="w-full h-1 bg-background rounded-sm opacity-30"
                        initial={{ opacity: 0 }}
                        animate={{ opacity: 1 }}
                        exit={{ opacity: 0 }}
                        transition={{ duration: 0.3 }}
                      />
                    )}
                  </React.Fragment>
                );
              })}
            </AnimatePresence>

            {currentLine < lines.length && (
              <motion.div
                initial={{ opacity: 0 }}
                animate={{ opacity: 1 }}
                className="flex items-center h-5"
                style={{ paddingLeft: `${lines[currentLine]?.segments[0]?.indent ? 16 : 0}px` }}
              >
                <motion.div
                  animate={{ opacity: cursorVisible ? 1 : 0 }}
                  transition={{ duration: 0.1 }}
                  className="w-0.5 h-3.5 bg-blue-500"
                />
              </motion.div>
            )}
          </div>
        </div>
      </motion.div>
    </div>
  );
};

export default ModernLoader;

Installation

npx shadcn@latest add @scrollxui/modern-loader

Usage

import { ModernLoader } from "@/components/modern-loader"
<ModernLoader />