{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "page-transition",
  "title": "Page Transition",
  "description": "GSAP-powered Next.js App Router page transition provider. Wraps your layout to animate between pages with a curtain reveal or slide push — ported from the Codrops async transition demo.",
  "dependencies": [
    "gsap",
    "next"
  ],
  "files": [
    {
      "path": "registry/components/unlumen/page-transition/index.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { usePathname, useRouter } from \"next/navigation\";\nimport gsap from \"gsap\";\nimport { CustomEase } from \"gsap/CustomEase\";\n\ngsap.registerPlugin(CustomEase);\n\n/* ------------------------------------------------------------------ */\n/*  Custom eases — exact bezier curves from the codrops demo           */\n/* ------------------------------------------------------------------ */\n\nconst EASE_CURTAIN = CustomEase.create(\n  \"pageTransition\",\n  \"M0,0 C0.38,0.05 0.48,0.58 0.65,0.82 0.82,1 1,1 1,1\",\n);\n\nconst EASE_SLIDE = CustomEase.create(\n  \"pageTransition2\",\n  \"M0,0 C0.178,0.031 0.279,0.802 0.345,0.856 0.421,0.918 0.374,1 1,1\",\n);\n\n/* ------------------------------------------------------------------ */\n/*  Types                                                               */\n/* ------------------------------------------------------------------ */\n\nexport type PageTransitionVariant = \"curtain\" | \"slide\";\n\nexport interface PageTransitionConfig {\n  /** Which transition animation to use. @default \"curtain\" */\n  variant?: PageTransitionVariant;\n  /** Duration in seconds. @default 0.7 for curtain, 1.5 for slide */\n  duration?: number;\n}\n\nexport interface PageTransitionProviderProps {\n  children: React.ReactNode;\n  config?: PageTransitionConfig;\n}\n\n/* ------------------------------------------------------------------ */\n/*  Context                                                             */\n/* ------------------------------------------------------------------ */\n\ninterface TransitionContextValue {\n  navigate: (href: string) => void;\n  isTransitioning: boolean;\n}\n\nconst TransitionContext = React.createContext<TransitionContextValue>({\n  navigate: () => {},\n  isTransitioning: false,\n});\n\nexport function usePageTransition() {\n  return React.useContext(TransitionContext);\n}\n\n/* ------------------------------------------------------------------ */\n/*  Transition animations — ported 1:1 from the codrops source         */\n/* ------------------------------------------------------------------ */\n\nfunction runCurtainTransition(\n  currentEl: HTMLElement,\n  nextEl: HTMLElement,\n  duration: number,\n): Promise<void> {\n  // Incoming page: position fixed, hidden behind a top clip\n  gsap.set(nextEl, {\n    clipPath: \"inset(100% 0% 0% 0%)\",\n    opacity: 1,\n    position: \"fixed\",\n    top: 0,\n    left: 0,\n    width: \"100%\",\n    height: \"100vh\",\n    zIndex: 10,\n    force3D: true,\n  });\n\n  return new Promise<void>((resolve) => {\n    const tl = gsap.timeline({ onComplete: resolve });\n\n    // Current page: scale down, move up, fade\n    tl.to(\n      currentEl,\n      {\n        y: \"-30vh\",\n        opacity: 0.4,\n        scale: 0.8,\n        duration,\n        force3D: true,\n        ease: EASE_CURTAIN,\n      },\n      0,\n    )\n      // Incoming page: curtain drops from top\n      .to(\n        nextEl,\n        {\n          clipPath: \"inset(0% 0% 0% 0%)\",\n          duration,\n          force3D: true,\n          ease: EASE_CURTAIN,\n        },\n        0,\n      );\n  });\n}\n\nfunction runSlideTransition(\n  currentEl: HTMLElement,\n  nextEl: HTMLElement,\n  duration: number,\n): Promise<void> {\n  // Incoming page: positioned to the right, ready to slide in\n  gsap.set(nextEl, {\n    opacity: 1,\n    position: \"fixed\",\n    top: 0,\n    left: 0,\n    width: \"100%\",\n    height: \"100vh\",\n    x: \"100%\",\n    zIndex: 10,\n    force3D: true,\n  });\n\n  return new Promise<void>((resolve) => {\n    const tl = gsap.timeline({ onComplete: resolve });\n\n    // Current page: push left + shrink + fade\n    tl.to(\n      currentEl,\n      {\n        x: \"-50%\",\n        scale: 0.8,\n        opacity: 0.4,\n        duration,\n        force3D: true,\n        ease: EASE_SLIDE,\n      },\n      0,\n    )\n      // Incoming page: slide in from right\n      .to(\n        nextEl,\n        {\n          x: 0,\n          duration,\n          force3D: true,\n          ease: EASE_SLIDE,\n        },\n        0,\n      );\n  });\n}\n\n/* ------------------------------------------------------------------ */\n/*  Provider                                                            */\n/* ------------------------------------------------------------------ */\n\nexport function PageTransitionProvider({\n  children,\n  config = {},\n}: PageTransitionProviderProps) {\n  const { variant = \"curtain\", duration } = config;\n  const effectiveDuration = duration ?? (variant === \"slide\" ? 1.5 : 0.7);\n\n  const router = useRouter();\n  const pathname = usePathname();\n  const wrapperRef = React.useRef<HTMLDivElement>(null);\n  const isTransitioningRef = React.useRef(false);\n  const [isTransitioning, setIsTransitioning] = React.useState(false);\n\n  // Store the latest config in a ref so the navigate callback is always fresh\n  const configRef = React.useRef({ variant, duration: effectiveDuration });\n  React.useEffect(() => {\n    configRef.current = { variant, duration: effectiveDuration };\n  }, [variant, effectiveDuration]);\n\n  const navigate = React.useCallback(\n    async (href: string) => {\n      if (isTransitioningRef.current) return;\n      if (href === window.location.pathname) return;\n\n      isTransitioningRef.current = true;\n      setIsTransitioning(true);\n\n      const wrapper = wrapperRef.current;\n      if (!wrapper) {\n        router.push(href);\n        isTransitioningRef.current = false;\n        setIsTransitioning(false);\n        return;\n      }\n\n      const currentContainer = wrapper.querySelector<HTMLElement>(\n        \"[data-pt-container]\",\n      );\n      if (!currentContainer) {\n        router.push(href);\n        isTransitioningRef.current = false;\n        setIsTransitioning(false);\n        return;\n      }\n\n      // Pre-fetch then fetch the next page HTML\n      const res = await fetch(href, { credentials: \"same-origin\" });\n      const html = await res.text();\n      const parser = new DOMParser();\n      const nextDoc = parser.parseFromString(html, \"text/html\");\n      const nextContent = nextDoc.querySelector(\"[data-pt-container]\");\n\n      if (!nextContent) {\n        router.push(href);\n        isTransitioningRef.current = false;\n        setIsTransitioning(false);\n        return;\n      }\n\n      // Clone and inject the next container alongside the current one\n      const nextContainer = nextContent.cloneNode(true) as HTMLElement;\n      wrapper.appendChild(nextContainer);\n\n      // Wait for images in the incoming page to load\n      const images = nextContainer.querySelectorAll<HTMLImageElement>(\"img\");\n      if (images.length > 0) {\n        await Promise.all(\n          Array.from(images).map(\n            (img) =>\n              new Promise<void>((resolve) => {\n                if (img.complete) return resolve();\n                img.onload = () => resolve();\n                img.onerror = () => resolve();\n              }),\n          ),\n        );\n      }\n\n      // Run the transition animation\n      const { variant: v, duration: d } = configRef.current;\n      const runner = v === \"slide\" ? runSlideTransition : runCurtainTransition;\n      await runner(currentContainer, nextContainer, d);\n\n      // Commit the navigation in the Next.js router\n      router.push(href);\n\n      // Clean up: remove the cloned container and reset styles\n      nextContainer.remove();\n      gsap.set(currentContainer, {\n        clearProps:\n          \"clipPath,position,top,left,width,height,zIndex,opacity,x,y,scale\",\n      });\n\n      isTransitioningRef.current = false;\n      setIsTransitioning(false);\n    },\n    [router],\n  );\n\n  return (\n    <TransitionContext.Provider value={{ navigate, isTransitioning }}>\n      <div ref={wrapperRef} style={{ position: \"relative\" }}>\n        <div data-pt-container>{children}</div>\n      </div>\n    </TransitionContext.Provider>\n  );\n}\n\n/* ------------------------------------------------------------------ */\n/*  TransitionLink — drop-in replacement for next/link                 */\n/* ------------------------------------------------------------------ */\n\nexport interface TransitionLinkProps\n  extends React.AnchorHTMLAttributes<HTMLAnchorElement> {\n  href: string;\n  children: React.ReactNode;\n}\n\nexport function TransitionLink({\n  href,\n  children,\n  onClick,\n  ...props\n}: TransitionLinkProps) {\n  const { navigate, isTransitioning } = usePageTransition();\n\n  const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {\n    // Let modified clicks (cmd+click, ctrl+click) pass through normally\n    if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;\n    e.preventDefault();\n    onClick?.(e);\n    navigate(href);\n  };\n\n  return (\n    <a\n      href={href}\n      onClick={handleClick}\n      aria-disabled={isTransitioning}\n      {...props}\n    >\n      {children}\n    </a>\n  );\n}\n\nexport default PageTransitionProvider;\n",
      "type": "registry:ui",
      "target": "components/unlumen-ui/page-transition.tsx"
    }
  ],
  "type": "registry:ui"
}