"use client"; import { useEffect, useRef, useState } from "react"; import { gsap } from "@/lib/gsap"; import { usePrefersReducedMotion } from "@/hooks/use-prefers-reduced-motion"; const CURL_PATH = "M 100 30 C 88 30, 76 26, 66 22 C 56 18, 46 18, 40 24 C 34 30, 38 40, 48 40 C 56 40, 60 32, 54 28 C 49 25, 43 29, 46 34"; const SESSION_KEY = "entscheidomat:preloader-seen"; /** * Minimalistische Ladeanimation: ein Kalligraphie-Ornament, * das sich selbst zeichnet, danach hebt sich der Vorhang. * Feuert "app:reveal", sobald die Seite sichtbar wird. */ export default function Preloader() { const reduced = usePrefersReducedMotion(); const [animationDone, setAnimationDone] = useState(false); const overlayRef = useRef(null); const leftCurlRef = useRef(null); const rightCurlRef = useRef(null); const dotRef = useRef(null); const wordRef = useRef(null); const hasStartedRef = useRef(false); const reveal = () => { document.documentElement.dataset.preloader = "complete"; window.dispatchEvent(new Event("app:reveal")); }; // Reduced Motion: keine Animation, aber Reveal-Event trotzdem dispatchen – // rein imperativ, kein setState, deshalb kein separater Effekt-Zweig nötig. useEffect(() => { let hasSeenPreloader = false; try { hasSeenPreloader = window.sessionStorage.getItem(SESSION_KEY) === "1"; } catch { // Falls Browser-Speicher blockiert ist, bleibt der Loader funktional. } const shouldSkip = document.documentElement.dataset.preloader === "cached"; if (reduced || shouldSkip || (hasSeenPreloader && !hasStartedRef.current)) { setAnimationDone(true); reveal(); return; } hasStartedRef.current = true; try { window.sessionStorage.setItem(SESSION_KEY, "1"); } catch { // Die Animation darf nicht von verfügbarem Browser-Speicher abhängen. } document.documentElement.style.overflow = "hidden"; const duration = window.matchMedia("(max-width: 767px)").matches ? 0.45 : 1; const curls = [leftCurlRef.current, rightCurlRef.current].filter( Boolean, ) as SVGPathElement[]; const lengths = curls.map((p) => p.getTotalLength()); curls.forEach((p, i) => { p.style.strokeDasharray = `${lengths[i]}`; p.style.strokeDashoffset = `${lengths[i]}`; }); const tl = gsap.timeline({ onComplete: () => { document.documentElement.style.overflow = ""; setAnimationDone(true); reveal(); }, }); tl.to(curls, { strokeDashoffset: 0, duration: 0.35 * duration, ease: "power2.inOut", stagger: 0.05 * duration, }) .fromTo( dotRef.current, { scale: 0, transformOrigin: "50% 50%" }, { scale: 1, duration: 0.15 * duration, ease: "back.out(3)" }, `-=${0.12 * duration}`, ) .fromTo( wordRef.current, { opacity: 0, y: 10, letterSpacing: "0.5em" }, { opacity: 1, y: 0, letterSpacing: "0.28em", duration: 0.22 * duration, ease: "power2.out" }, `-=${0.1 * duration}`, ) .to(overlayRef.current, { yPercent: -100, duration: 0.4 * duration, ease: "power4.inOut", delay: 0.08 * duration, }); return () => { document.documentElement.style.overflow = ""; tl.kill(); }; }, [reduced]); if (reduced || animationDone) return null; return (

Entscheidomat

); }