{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"page-transition","type":"registry:component","title":"Page Transition","description":"A route transition engine that covers the page during navigation.","meta":{"pro":false},"dependencies":["@react-three/drei"],"registryDependencies":["https://atelier-ui.com/r/agent-rules.json"],"files":[{"path":"registry/page-transition/page-transition.tsx","type":"registry:component","target":"components/page-transition/page-transition.tsx","content":"\"use client\"\n\nimport { useProgress, useTexture } from \"@react-three/drei\"\nimport Link from \"next/link\"\nimport { usePathname, useRouter } from \"next/navigation\"\nimport {\n    type ComponentProps,\n    type ComponentType,\n    createContext,\n    type ReactNode,\n    useCallback,\n    useContext,\n    useEffect,\n    useRef,\n    useState,\n    useTransition,\n} from \"react\"\n\nexport type CoverAnimation = (reduced: boolean) => Promise<void>\ntype PageComponent = ComponentType<{ children: ReactNode }>\ntype TransitionLinkProps = Omit<ComponentProps<typeof Link>, \"href\"> & {\n    href: string\n    preload?: string[]\n}\n\nconst trimSlash = (path: string) => path.replace(/(.)\\/+$/, \"$1\")\n\nconst NavigateContext = createContext<((href: string) => void) | null>(null)\nconst PageContext = createContext<PageComponent | null>(null)\n\ntype TransitionProviderProps = {\n    children: ReactNode\n    coverIn: CoverAnimation\n    coverOut: CoverAnimation\n    page?: PageComponent\n}\n\nexport function TransitionProvider({ children, coverIn, coverOut, page }: TransitionProviderProps) {\n    const router = useRouter()\n    const pathname = usePathname()\n    const busy = useRef(false)\n    const reduced = useRef(false)\n    const animations = useRef({ coverIn, coverOut })\n    animations.current = { coverIn, coverOut }\n    const [covered, setCovered] = useState(false)\n    const [isPending, startTransition] = useTransition()\n    const loaderActive = useProgress((state) => state.active)\n\n    const locationPath = useRef(\"\")\n    useEffect(() => {\n        locationPath.current = window.location.pathname\n    }, [pathname])\n\n    const navigate = useCallback(\n        async (href: string) => {\n            if (busy.current) return\n            const target = new URL(href, `http://n${pathname}`).pathname\n            if (trimSlash(target) === trimSlash(pathname)) return\n\n            busy.current = true\n            router.prefetch(href)\n            document.documentElement.setAttribute(\"data-atelier-transitioning\", \"\")\n            reduced.current = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n            await animations.current.coverIn(reduced.current)\n            startTransition(() => router.push(href))\n            setCovered(true)\n        },\n        [router, pathname],\n    )\n\n    useEffect(() => {\n        const onPopState = (event: PopStateEvent) => {\n            if (busy.current) return\n            if (window.location.pathname === locationPath.current) return\n\n            event.stopImmediatePropagation()\n            busy.current = true\n            document.documentElement.setAttribute(\"data-atelier-transitioning\", \"\")\n            const href = window.location.href\n            reduced.current = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n            animations.current.coverIn(reduced.current).then(() => {\n                startTransition(() => router.replace(href, { scroll: false }))\n                setCovered(true)\n            })\n        }\n\n        window.addEventListener(\"popstate\", onPopState, { capture: true })\n        return () => {\n            window.removeEventListener(\"popstate\", onPopState, { capture: true })\n            document.documentElement.removeAttribute(\"data-atelier-transitioning\")\n        }\n    }, [router])\n\n    /*\n     * After the route commits, WebGL planes mount into the canvas a frame\n     * later, so asset loading has not started yet when this effect first\n     * runs. The reveal waits two frames and re-checks the live store: if\n     * loading began, the effect re-runs when it ends.\n     */\n    useEffect(() => {\n        if (!covered || isPending || loaderActive) return\n\n        let cancelled = false\n        const raf = requestAnimationFrame(() => {\n            requestAnimationFrame(() => {\n                if (cancelled || useProgress.getState().active) return\n                setCovered(false)\n                animations.current.coverOut(reduced.current).then(() => {\n                    document.documentElement.removeAttribute(\"data-atelier-transitioning\")\n                    busy.current = false\n                })\n            })\n        })\n\n        return () => {\n            cancelled = true\n            cancelAnimationFrame(raf)\n        }\n    }, [covered, isPending, loaderActive])\n\n    return (\n        <NavigateContext.Provider value={navigate}>\n            <PageContext.Provider value={page ?? null}>{children}</PageContext.Provider>\n        </NavigateContext.Provider>\n    )\n}\n\nexport function TransitionPage({ children }: { children: ReactNode }) {\n    const Page = useContext(PageContext)\n    return Page ? <Page>{children}</Page> : children\n}\n\nexport function TransitionLink({ href, preload, ...rest }: TransitionLinkProps) {\n    const navigate = useContext(NavigateContext)\n\n    const preloadAssets = () => {\n        for (const url of preload ?? []) useTexture.preload(url)\n    }\n\n    return (\n        <Link\n            {...rest}\n            href={href}\n            onPointerEnter={(event) => {\n                rest.onPointerEnter?.(event)\n                preloadAssets()\n            }}\n            onNavigate={(event) => {\n                if (!navigate) return\n                event.preventDefault()\n                preloadAssets()\n                navigate(href)\n            }}\n        />\n    )\n}\n"}]}