'use client'; import React, { createContext, useContext, useState, useCallback, ReactNode, useEffect } from 'react'; import { useRouter, usePathname, useSearchParams } from 'next/navigation'; import { LazyMotion } from 'framer-motion'; import dynamic from 'next/dynamic'; const loadFeatures = () => import('@/lib/framer-features').then(res => res.default); // Defer loading the shutter to eliminate AnimatePresence from the initial bundle const DynamicPageTransitionShutter = dynamic( () => import('./PageTransitionShutter').then(mod => mod.PageTransitionShutter), { ssr: false } ); interface TransitionContextType { isTransitioning: boolean; transitionMessage: string | null; startTransition: (href: string, message?: string | null) => void; } const TransitionContext = createContext(undefined); export function TransitionProvider({ children }: { children: ReactNode }) { const [isTransitioning, setIsTransitioning] = useState(false); const [transitionMessage, setTransitionMessage] = useState(null); const [hasMountedShutter, setHasMountedShutter] = useState(false); const router = useRouter(); // Load the heavy shutter chunk in the background when the main thread is idle useEffect(() => { if (typeof window !== 'undefined' && 'requestIdleCallback' in window) { window.requestIdleCallback(() => setHasMountedShutter(true)); } else { setTimeout(() => setHasMountedShutter(true), 2000); } }, []); // We use a ref to track transition state without triggering the route change effect when it turns true const isTransitioningRef = React.useRef(isTransitioning); useEffect(() => { isTransitioningRef.current = isTransitioning; }, [isTransitioning]); const startTransition = useCallback((href: string, message: string | null = null) => { // Wenn wir bereits navigieren, nichts tun if (isTransitioning) return; // Normalize paths to avoid false mismatches (e.g. /de vs /de/) const currentPath = typeof window !== 'undefined' ? window.location.pathname.replace(/\/$/, '') || '/' : ''; const targetPath = href.split('?')[0].replace(/\/$/, '') || '/'; const currentSearch = typeof window !== 'undefined' ? window.location.search : ''; const targetSearch = href.includes('?') ? '?' + href.split('?')[1] : ''; // Aktuelle URL prüfen, falls es dieselbe ist, nicht navigieren if (currentPath === targetPath && currentSearch === targetSearch) { // Optional: Wir könnten weich nach oben scrollen window.scrollTo({ top: 0, behavior: 'smooth' }); return; } setTransitionMessage(message); setIsTransitioning(true); setHasMountedShutter(true); // Die Animation des Shutters abwarten (ca. 700ms), dann pushen setTimeout(() => { router.push(href); // Fallback: If for some reason the route doesn't change after 60 seconds (useful for slow dev servers), open the shutter anyway setTimeout(() => { setIsTransitioning((current) => { if (current) { setTimeout(() => setTransitionMessage(null), 300); return false; } return current; }); }, 60000); }, 700); }, [isTransitioning, router]); return ( {hasMountedShutter && } {children} ); } function TransitionRouteWatcher({ isTransitioningRef, setIsTransitioning, setTransitionMessage }: any) { const pathname = usePathname(); const searchParams = useSearchParams(); useEffect(() => { if (isTransitioningRef.current) { const timer = setTimeout(() => { setIsTransitioning(false); setTimeout(() => setTransitionMessage(null), 300); }, 100); return () => clearTimeout(timer); } }, [pathname, searchParams, setIsTransitioning, setTransitionMessage, isTransitioningRef]); return null; } export function useTransition() { const context = useContext(TransitionContext); if (!context) { throw new Error('useTransition must be used within a TransitionProvider'); } return context; }