{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "preloader",
  "title": "Preloader",
  "description": "Full-screen loader with a counting percentage and rotating quotes that reveals the page at 100%.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/new-york/preloader/preloader.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useRef, useState } from \"react\"\nimport { AnimatePresence, animate, motion, useMotionValue, useReducedMotion, useTransform } from \"motion/react\"\nimport { cn } from \"@/lib/utils\"\n\nexport interface PreloaderQuote {\n  text: string\n  author?: string\n}\n\nexport interface PreloaderProps {\n  /** Page content, rendered underneath and revealed when loading finishes. */\n  children?: React.ReactNode\n  /** Quotes that cycle while loading. */\n  quotes?: PreloaderQuote[]\n  /** Length of the simulated load, in seconds. Default: 3 */\n  duration?: number\n  /** Controlled progress, 0 to 100. When set, the counter follows it instead of simulating. */\n  progress?: number\n  /** How the loader leaves. Default: \"curtain\" */\n  exit?: \"curtain\" | \"split\" | \"fade\"\n  /** Overlay background (any CSS color). Inverts the theme by default. Default: \"var(--foreground)\" */\n  background?: string\n  /** Text color. Default: \"var(--background)\" */\n  color?: string\n  /** Accent used for the progress line. Default: \"#ff6a2b\" */\n  accent?: string\n  /** Show the big percentage counter. Default: true */\n  showCounter?: boolean\n  /** Prevent page scrolling while loading. Default: true */\n  lockScroll?: boolean\n  /** \"fixed\" covers the viewport; \"absolute\" covers the nearest positioned parent. Default: \"fixed\" */\n  position?: \"fixed\" | \"absolute\"\n  /** Called once the exit animation has finished. */\n  onComplete?: () => void\n  className?: string\n}\n\nconst DEFAULT_QUOTES: PreloaderQuote[] = [\n  { text: \"Motion is the language of change.\", author: \"—\" },\n  { text: \"Good design is as little design as possible.\", author: \"Dieter Rams\" },\n  { text: \"Details are not the details. They make the design.\", author: \"Charles Eames\" },\n]\n\n// Uneven pacing reads as \"real\" loading rather than a linear timer\nconst LOAD_EASE = [0.65, 0.05, 0.36, 1] as const\n\nexport function Preloader({\n  children,\n  quotes = DEFAULT_QUOTES,\n  duration = 3,\n  progress,\n  exit = \"curtain\",\n  background = \"var(--foreground)\",\n  color = \"var(--background)\",\n  accent = \"#ff6a2b\",\n  showCounter = true,\n  lockScroll = true,\n  position = \"fixed\",\n  onComplete,\n  className,\n}: PreloaderProps) {\n  const reduced = useReducedMotion()\n  const [phase, setPhase] = useState<\"loading\" | \"leaving\" | \"done\">(\"loading\")\n  const [quote, setQuote] = useState(0)\n  const value = useMotionValue(0)\n  const rounded = useTransform(value, (v) => Math.floor(v).toString().padStart(2, \"0\"))\n  const lineScale = useTransform(value, (v) => v / 100)\n  const onCompleteRef = useRef(onComplete)\n\n  useEffect(() => {\n    onCompleteRef.current = onComplete\n  }, [onComplete])\n\n  // Simulated progress\n  useEffect(() => {\n    if (progress !== undefined) return\n    const controls = animate(value, 100, {\n      duration: reduced ? 0.4 : duration,\n      ease: LOAD_EASE,\n      onComplete: () => setTimeout(() => setPhase(\"leaving\"), reduced ? 0 : 350),\n    })\n    return () => controls.stop()\n  }, [progress, duration, reduced, value])\n\n  // Controlled progress\n  useEffect(() => {\n    if (progress === undefined) return\n    const target = Math.min(Math.max(progress, 0), 100)\n    const controls = animate(value, target, {\n      duration: 0.5,\n      ease: \"easeOut\",\n      onComplete: () => target >= 100 && setTimeout(() => setPhase(\"leaving\"), 350),\n    })\n    return () => controls.stop()\n  }, [progress, value])\n\n  // Cycle quotes evenly across the load\n  useEffect(() => {\n    if (phase !== \"loading\" || quotes.length < 2 || progress !== undefined) return\n    const every = (duration * 1000) / quotes.length\n    const id = setInterval(() => setQuote((q) => Math.min(q + 1, quotes.length - 1)), every)\n    return () => clearInterval(id)\n  }, [phase, quotes.length, duration, progress])\n\n  useEffect(() => {\n    if (!lockScroll || position !== \"fixed\" || phase === \"done\") return\n    const prev = document.documentElement.style.overflow\n    document.documentElement.style.overflow = \"hidden\"\n    return () => {\n      document.documentElement.style.overflow = prev\n    }\n  }, [lockScroll, position, phase])\n\n  const finish = () => {\n    setPhase(\"done\")\n    onCompleteRef.current?.()\n  }\n\n  const exitAnim =\n    reduced || exit === \"fade\"\n      ? { opacity: 0 }\n      : exit === \"split\"\n        ? { clipPath: \"inset(50% 0% 50% 0%)\" }\n        : { clipPath: \"inset(0% 0% 100% 0%)\" }\n\n  const current = quotes[quote]\n\n  return (\n    <>\n      {children}\n      {phase !== \"done\" && (\n        <motion.div\n          role=\"progressbar\"\n          aria-label=\"Loading\"\n          aria-valuemin={0}\n          aria-valuemax={100}\n          className={cn(\n            \"inset-0 z-[9998] flex flex-col justify-between overflow-hidden p-6 [container-type:size] sm:p-10\",\n            position,\n            className\n          )}\n          style={{ background, color, clipPath: \"inset(0% 0% 0% 0%)\" }}\n          animate={phase === \"leaving\" ? exitAnim : undefined}\n          transition={{ duration: reduced ? 0.3 : 0.9, ease: [0.76, 0, 0.24, 1] }}\n          onAnimationComplete={() => phase === \"leaving\" && finish()}\n        >\n          {/* quote */}\n          <div className=\"mx-auto mt-[18cqh] w-full max-w-xl text-center\">\n            <AnimatePresence mode=\"wait\">\n              {current && (\n                <motion.figure\n                  key={quote}\n                  initial=\"hidden\"\n                  animate={phase === \"leaving\" ? \"exit\" : \"visible\"}\n                  exit=\"exit\"\n                  variants={{\n                    hidden: {},\n                    visible: { transition: { staggerChildren: reduced ? 0 : 0.045 } },\n                    exit: { opacity: 0, y: -12, filter: \"blur(6px)\", transition: { duration: 0.35 } },\n                  }}\n                >\n                  <blockquote className=\"text-balance text-xl font-medium leading-snug tracking-tight sm:text-2xl\">\n                    {current.text.split(\" \").map((word, i) => (\n                      <motion.span\n                        key={i}\n                        className=\"mr-[0.25em] inline-block\"\n                        variants={{\n                          hidden: { opacity: 0, y: 10, filter: \"blur(8px)\" },\n                          visible: { opacity: 1, y: 0, filter: \"blur(0px)\", transition: { duration: 0.5, ease: [0.16, 1, 0.3, 1] } },\n                        }}\n                      >\n                        {word}\n                      </motion.span>\n                    ))}\n                  </blockquote>\n                  {current.author && (\n                    <motion.figcaption\n                      className=\"mt-4 font-mono text-[11px] uppercase tracking-[0.2em] opacity-50\"\n                      variants={{ hidden: { opacity: 0 }, visible: { opacity: 0.5, transition: { delay: 0.3 } } }}\n                    >\n                      {current.author}\n                    </motion.figcaption>\n                  )}\n                </motion.figure>\n              )}\n            </AnimatePresence>\n          </div>\n\n          {/* counter + progress line */}\n          <div>\n            {showCounter && (\n              <motion.div\n                className=\"flex items-end justify-between\"\n                animate={phase === \"leaving\" && !reduced ? { y: -40, opacity: 0 } : undefined}\n                transition={{ duration: 0.5, ease: [0.76, 0, 0.24, 1] }}\n              >\n                <span className=\"font-mono text-[11px] uppercase tracking-[0.2em] opacity-50\">Loading</span>\n                <span className=\"flex items-start font-semibold leading-[0.8] tracking-[-0.06em] tabular-nums\">\n                  <motion.span className=\"text-[clamp(4rem,22cqw,15rem)]\">{rounded}</motion.span>\n                  <span className=\"mt-[0.3em] text-[clamp(1.25rem,3.5cqw,2.5rem)] opacity-60\">%</span>\n                </span>\n              </motion.div>\n            )}\n            <div className=\"mt-6 h-px w-full overflow-hidden\" style={{ background: `color-mix(in oklab, ${color} 15%, transparent)` }}>\n              <motion.div className=\"h-full origin-left\" style={{ scaleX: lineScale, background: accent }} />\n            </div>\n          </div>\n        </motion.div>\n      )}\n    </>\n  )\n}\n",
      "type": "registry:component"
    }
  ],
  "docs": "Docs, live playground and every prop: https://trytweenly.vercel.app/docs/components/preloader",
  "type": "registry:component"
}