{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "loader",
  "type": "registry:ui",
  "title": "Loader",
  "description": "A cinematic loader with a traveling numeric counter and a smooth staggered curtain reveal.",
  "dependencies": [
    "gsap",
    "@gsap/react",
    "clsx",
    "tailwind-merge"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/registry/ui/loader.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef, useState, type ReactNode } from \"react\";\nimport { useGSAP } from \"@gsap/react\";\nimport gsap from \"gsap\";\nimport { cn } from \"@/lib/utils\";\n\ngsap.registerPlugin(useGSAP);\n\nexport interface LoaderProps {\n  children?: ReactNode;\n  progress?: number;\n  duration?: number;\n  revealDuration?: number;\n  stagger?: number;\n  columns?: number;\n  background?: string;\n  foreground?: string;\n  label?: string;\n  className?: string;\n  onComplete?: () => void;\n}\n\nconst bounded = (value: number, fallback: number, min: number, max: number) =>\n  Number.isFinite(value) ? Math.min(max, Math.max(min, value)) : fallback;\n\nexport function Loader({\n  children,\n  progress,\n  duration = 2.2,\n  revealDuration = 0.85,\n  stagger = 0.09,\n  columns = 5,\n  background = \"#191919\",\n  foreground = \"#f2f0e9\",\n  label = \"Loading\",\n  className,\n  onComplete,\n}: LoaderProps) {\n  const rootRef = useRef<HTMLDivElement>(null);\n  const overlayRef = useRef<HTMLDivElement>(null);\n  const contentRef = useRef<HTMLDivElement>(null);\n  const counterRef = useRef<HTMLDivElement>(null);\n  const numberRef = useRef<HTMLSpanElement>(null);\n  const controllerRef = useRef<((value: number) => void) | null>(null);\n  const callbackRef = useRef(onComplete);\n  const progressRef = useRef(progress);\n  const finishedRef = useRef(false);\n  const [complete, setComplete] = useState(false);\n  const automatic = progress === undefined;\n  const panelCount = Math.round(bounded(columns, 5, 2, 12));\n  const loadingDuration = bounded(duration, 2.2, 0.3, 30);\n  const exitDuration = bounded(revealDuration, 0.85, 0.2, 3);\n  const panelStagger = bounded(stagger, 0.09, 0, 0.3);\n\n  useEffect(() => {\n    callbackRef.current = onComplete;\n    progressRef.current = progress;\n  }, [onComplete, progress]);\n\n  useGSAP(\n    () => {\n      const media = gsap.matchMedia();\n      finishedRef.current = false;\n      setComplete(false);\n\n      media.add(\n        {\n          reduceMotion: \"(prefers-reduced-motion: reduce)\",\n          animate: \"(prefers-reduced-motion: no-preference)\",\n        },\n        (context) => {\n          const overlay = overlayRef.current;\n          const content = contentRef.current;\n          const counter = counterRef.current;\n          const number = numberRef.current;\n          if (!overlay || !content || !counter || !number) return;\n\n          const panels = gsap.utils.toArray<HTMLElement>(\n            \"[data-loader-panel]\",\n            rootRef.current,\n          );\n          const reduced = Boolean(context.conditions?.reduceMotion);\n          const state = { value: 0 };\n          let revealing = false;\n          let progressTween: gsap.core.Tween | null = null;\n          const moveCounter = gsap.quickSetter(counter, \"xPercent\");\n          const alignNumber = gsap.quickSetter(number, \"xPercent\");\n\n          const renderProgress = () => {\n            const value = Math.round(state.value);\n            number.textContent = String(value).padStart(2, \"0\");\n            overlay.setAttribute(\"aria-valuenow\", String(value));\n            moveCounter(reduced ? 0 : state.value);\n            alignNumber(reduced ? 0 : -state.value);\n          };\n\n          const finish = () => {\n            if (finishedRef.current) return;\n            finishedRef.current = true;\n            setComplete(true);\n            callbackRef.current?.();\n          };\n\n          const reveal = context.add(\"reveal\", () => {\n            if (revealing || finishedRef.current) return;\n            revealing = true;\n            if (reduced) {\n              gsap.set(overlay, { autoAlpha: 0 });\n              gsap.set(content, { y: 0 });\n              finish();\n              return;\n            }\n\n            gsap\n              .timeline({\n                defaults: { ease: \"power3.inOut\" },\n                onComplete: finish,\n              })\n              .to(counter, { autoAlpha: 0, y: -16, duration: 0.18 })\n              .to(\n                panels,\n                { yPercent: -100, duration: exitDuration, stagger: panelStagger },\n                \">+=0.12\",\n              )\n              .to(\n                content,\n                { y: 0, duration: exitDuration, ease: \"power3.out\" },\n                \"<+=0.12\",\n              )\n              .set(overlay, { autoAlpha: 0 });\n          }) as () => void;\n\n          gsap.set(overlay, { autoAlpha: finishedRef.current ? 0 : 1 });\n          gsap.set(content, { y: reduced || finishedRef.current ? 0 : 24 });\n          gsap.set(counter, { autoAlpha: 1, y: 0 });\n          gsap.set(panels, { yPercent: 0 });\n          renderProgress();\n\n          const updateProgress = context.add(\"updateProgress\", (value: number) => {\n            if (revealing || finishedRef.current) return;\n            const target = bounded(value, 0, 0, 100);\n            progressTween?.kill();\n            if (reduced) {\n              state.value = target;\n              renderProgress();\n              if (target === 100) reveal();\n              return;\n            }\n            progressTween = gsap.to(state, {\n              value: target,\n              duration: 0.45,\n              ease: \"power2.out\",\n              onUpdate: renderProgress,\n              onComplete: () => {\n                if (target === 100) reveal();\n              },\n            });\n          }) as (value: number) => void;\n\n          controllerRef.current = updateProgress;\n          if (!finishedRef.current) {\n            if (automatic) {\n              if (reduced) updateProgress(100);\n              else {\n                gsap\n                  .timeline({\n                    defaults: { ease: \"power2.inOut\" },\n                    onUpdate: renderProgress,\n                    onComplete: reveal,\n                  })\n                  .to(state, { value: 16, duration: loadingDuration * 0.14 })\n                  .to(state, { value: 16, duration: loadingDuration * 0.07 })\n                  .to(state, { value: 40, duration: loadingDuration * 0.18 })\n                  .to(state, { value: 40, duration: loadingDuration * 0.09 })\n                  .to(state, { value: 61, duration: loadingDuration * 0.16 })\n                  .to(state, { value: 61, duration: loadingDuration * 0.08 })\n                  .to(state, { value: 100, duration: loadingDuration * 0.2 })\n                  .to(state, { value: 100, duration: loadingDuration * 0.08 });\n              }\n            } else updateProgress(progressRef.current ?? 0);\n          }\n\n          return () => {\n            progressTween?.kill();\n            controllerRef.current = null;\n          };\n        },\n        rootRef,\n      );\n\n      return () => media.revert();\n    },\n    {\n      scope: rootRef,\n      dependencies: [\n        automatic,\n        panelCount,\n        loadingDuration,\n        exitDuration,\n        panelStagger,\n      ],\n      revertOnUpdate: true,\n    },\n  );\n\n  useGSAP(\n    () => {\n      if (progress !== undefined) controllerRef.current?.(progress);\n    },\n    { scope: rootRef, dependencies: [progress] },\n  );\n\n  return (\n    <div\n      ref={rootRef}\n      aria-busy={!complete}\n      className={cn(\n        \"relative isolate min-h-80 w-full overflow-hidden [container-type:inline-size]\",\n        className,\n      )}\n    >\n      <div\n        ref={contentRef}\n        inert={!complete}\n        aria-hidden={!complete}\n        className=\"relative min-h-[inherit]\"\n      >\n        {children}\n      </div>\n      <div\n        ref={overlayRef}\n        role=\"progressbar\"\n        aria-label={label}\n        aria-valuemin={0}\n        aria-valuemax={100}\n        aria-valuenow={0}\n        aria-hidden={complete}\n        className={cn(\n          \"absolute inset-0 z-20 overflow-hidden\",\n          complete && \"pointer-events-none\",\n        )}\n        style={{ color: foreground }}\n      >\n        <div aria-hidden=\"true\" className=\"absolute inset-0 flex\">\n          {Array.from({ length: panelCount }, (_, index) => (\n            <div\n              key={index}\n              data-loader-panel\n              className=\"h-full min-w-0 flex-1 will-change-transform\"\n              style={{\n                backgroundColor: background,\n                marginRight: index < panelCount - 1 ? -1 : 0,\n              }}\n            />\n          ))}\n        </div>\n        <div aria-hidden=\"true\" className=\"absolute inset-x-[6%] top-[7%]\">\n          <div ref={counterRef} className=\"w-full will-change-transform\">\n            <span\n              ref={numberRef}\n              className=\"inline-block text-[clamp(3.5rem,13cqw,10rem)] leading-none font-semibold tracking-[-0.075em] tabular-nums will-change-transform\"\n            >\n              00\n            </span>\n          </div>\n        </div>\n      </div>\n      <span role=\"status\" className=\"sr-only\">\n        {complete ? \"Content ready\" : label}\n      </span>\n    </div>\n  );\n}\n\nexport default Loader;\n",
      "type": "registry:ui",
      "target": "@ui/loader.tsx"
    }
  ]
}
