Files
entscheidomat/components/hero.tsx
2026-07-23 21:59:15 +02:00

328 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useEffect, useRef, useState } from "react";
import {
motion,
useMotionValue,
useScroll,
useSpring,
useTransform,
} from "framer-motion";
import { ArrowDown } from "lucide-react";
import { gsap } from "@/lib/gsap";
export default function Hero() {
const sectionRef = useRef<HTMLElement>(null);
const { scrollY } = useScroll();
// Scroll-Transformationen (Reversibel)
const ringY = useTransform(scrollY, [0, 1000], [0, 180]);
const ringScale = useTransform(scrollY, [0, 1000], [1, 1.18]);
const ringOpacity = useTransform(scrollY, [0, 400, 1000, 1500], [1, 1, 0.5, 0]);
const ringRotate = useTransform(scrollY, [0, 1000], [0, 10]);
// Interaktive Cursor-Bewegung (Subtile Spring-Physik für elegantes Hovering)
const rawMouseX = useMotionValue(0);
const rawMouseY = useMotionValue(0);
const smoothMouseX = useSpring(rawMouseX, { stiffness: 60, damping: 28 });
const smoothMouseY = useSpring(rawMouseY, { stiffness: 60, damping: 28 });
// Subtile magnetische Linienverschiebung
const magnetX = useTransform(smoothMouseX, [-0.5, 0.5], [-10, 10]);
const magnetY = useTransform(smoothMouseY, [-0.5, 0.5], [-8, 8]);
// SVG-Glow-Position (in viewBox 1550 x 950 Koordinaten)
const [svgGlow, setSvgGlow] = useState({ x: 775, y: 475 });
const handleMouseMove = (e: React.MouseEvent<HTMLElement>) => {
const rect = sectionRef.current?.getBoundingClientRect();
if (!rect) return;
// Normalisierte Mauskoordinaten (-0.5 bis 0.5)
const normX = (e.clientX - rect.left) / rect.width - 0.5;
const normY = (e.clientY - rect.top) / rect.height - 0.5;
rawMouseX.set(normX);
rawMouseY.set(normY);
// Umrechnung auf SVG-ViewBox-Koordinaten für das sanfte Aufleuchten
const svgX = Math.round(((e.clientX - rect.left) / rect.width) * 1550);
const svgY = Math.round(((e.clientY - rect.top) / rect.height) * 950);
setSvgGlow({ x: svgX, y: svgY });
};
// Text-Intro startet erst, wenn der Preloader das "app:reveal"-Event feuert
useEffect(() => {
// Der Hero ist das LCP-Element. Auf Mobilgeräten bleibt er deshalb sofort
// sichtbar, statt seine endgültige Darstellung durch eine Timeline zu verzögern.
if (window.matchMedia("(max-width: 767px)").matches) return;
let ctx: gsap.Context | undefined;
let fallback: ReturnType<typeof setTimeout> | undefined;
const start = () => {
if (ctx) return;
ctx = gsap.context(() => {
gsap.fromTo(
".hero-el",
{ y: 34, opacity: 0 },
{
y: 0,
opacity: 1,
duration: 1,
stagger: 0.1,
ease: "power3.out",
},
);
}, sectionRef);
};
const mm = gsap.matchMedia();
mm.add("(prefers-reduced-motion: no-preference)", () => {
window.addEventListener("app:reveal", start, { once: true });
// Beim gecachten bzw. mobilen Preloader ist die Seite schon sichtbar,
// bevor dieser Effect registriert wird. Dann sofort animieren statt auf
// den Fallback-Timer zu warten.
if (document.documentElement.dataset.preloader !== "active") {
start();
} else {
fallback = setTimeout(start, 1200);
}
});
return () => {
mm.revert();
window.removeEventListener("app:reveal", start);
if (fallback) clearTimeout(fallback);
ctx?.revert();
};
}, []);
return (
<section
id="top"
ref={sectionRef}
onMouseMove={handleMouseMove}
className="relative flex min-h-[100svh] flex-col justify-center pt-16 cursor-default"
>
{/* Weicher Spot in der Mitte: Fadet am Rand und unten zu 100% transparent in den Seitenhintergrund aus */}
<div
aria-hidden
className="absolute inset-0 bg-[radial-gradient(ellipse_75%_55%_at_50%_35%,#ffffff_0%,transparent_100%)]"
/>
{/* Multischichtiges Ring-Design: Kreise fließen flüssig über die Sektionsgrenze nach unten */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 1.5, ease: "easeOut" }}
style={{
y: ringY,
x: magnetX,
scale: ringScale,
opacity: ringOpacity,
rotate: ringRotate,
}}
className="pointer-events-none absolute left-1/2 top-1/2 w-[min(100vw,1550px)] h-[min(115vh,1100px)] -translate-x-1/2 -translate-y-1/2"
>
<motion.div style={{ y: magnetY }} className="w-full h-full">
<svg
aria-hidden
className="w-full h-full"
viewBox="0 0 1550 1100"
fill="none"
>
<defs>
{/* Subtiler radialer Glow-Verlauf unter dem Cursor */}
<radialGradient
id="cursorLineGlow"
cx={svgGlow.x}
cy={svgGlow.y}
r="280"
gradientUnits="userSpaceOnUse"
>
<stop offset="0%" stopColor="#0d7a57" stopOpacity="0.32" />
<stop offset="50%" stopColor="#0d7a57" stopOpacity="0.12" />
<stop offset="100%" stopColor="#0d7a57" stopOpacity="0" />
</radialGradient>
</defs>
{/* 1. Basis-Ellipsen mit dezentem Ruhe-Opacity */}
<ellipse
cx="775"
cy="490"
rx="290"
ry="180"
stroke="#111412"
strokeOpacity="0.13"
strokeWidth="1"
/>
<ellipse
cx="775"
cy="490"
rx="440"
ry="275"
stroke="#111412"
strokeOpacity="0.10"
strokeWidth="1"
/>
<ellipse
cx="775"
cy="490"
rx="610"
ry="370"
stroke="#111412"
strokeOpacity="0.11"
strokeWidth="1"
/>
<ellipse
cx="775"
cy="490"
rx="770"
ry="460"
stroke="#111412"
strokeOpacity="0.08"
strokeWidth="1"
strokeDasharray="5 5"
/>
<ellipse
cx="775"
cy="490"
rx="920"
ry="550"
stroke="#111412"
strokeOpacity="0.06"
strokeWidth="1"
/>
<ellipse
cx="775"
cy="490"
rx="1080"
ry="640"
stroke="#111412"
strokeOpacity="0.05"
strokeWidth="1"
/>
{/* Dynamisch kreuzende Orbit-Bögen reichen flüssig bis in die nächste Sektion */}
<circle
cx="690"
cy="440"
r="620"
stroke="#111412"
strokeOpacity="0.07"
strokeWidth="1"
/>
<circle
cx="860"
cy="570"
r="680"
stroke="#111412"
strokeOpacity="0.06"
strokeWidth="1"
/>
<ellipse
cx="775"
cy="490"
rx="1040"
ry="440"
transform="rotate(-11 775 490)"
stroke="#111412"
strokeOpacity="0.06"
strokeWidth="1"
/>
{/* Feine Horizon-Linie hinter den Buttons */}
<line
x1="390"
y1="690"
x2="1160"
y2="690"
stroke="#111412"
strokeOpacity="0.09"
strokeWidth="1"
/>
{/* 2. Subtile Aufleuchten-Ebene unter dem Cursor */}
<g stroke="url(#cursorLineGlow)" strokeWidth="1.3">
<ellipse cx="775" cy="490" rx="290" ry="180" />
<ellipse cx="775" cy="490" rx="440" ry="275" />
<ellipse cx="775" cy="490" rx="610" ry="370" />
<ellipse cx="775" cy="490" rx="770" ry="460" strokeDasharray="5 5" />
<ellipse cx="775" cy="490" rx="920" ry="550" />
<ellipse cx="775" cy="490" rx="1080" ry="640" />
<circle cx="690" cy="440" r="620" />
<circle cx="860" cy="570" r="680" />
<ellipse
cx="775"
cy="490"
rx="1040"
ry="440"
transform="rotate(-11 775 490)"
/>
<line x1="390" y1="690" x2="1160" y2="690" />
</g>
</svg>
</motion.div>
</motion.div>
<div className="bg-noise pointer-events-none absolute inset-0" aria-hidden />
<div className="relative mx-auto flex w-full max-w-5xl flex-col items-center px-5 py-16 text-center sm:px-8">
<h1 className="font-sans text-[11vw] font-medium leading-[1.02] tracking-[-0.03em] sm:text-6xl lg:text-7xl">
<span className="hero-el block">Kostenloser</span>
<span className="hero-el block font-display text-[1.06em] font-normal italic tracking-normal text-[#0d7a57]">
Zufallsgenerator
</span>
<span className="hero-el block">für schnelle Entscheidungen</span>
</h1>
<p className="hero-el mt-8 max-w-md text-base leading-relaxed text-ink-soft sm:text-lg">
Lass den Zufall entscheiden mit Münze, Glücksrad, Würfel &amp; mehr für
alle, die sich mal wieder nicht entscheiden können.
</p>
<div className="hero-el mt-10 flex flex-wrap items-center justify-center gap-3">
<motion.a
href="#tools"
whileHover={{ y: -2, scale: 1.02 }}
whileTap={{ scale: 0.97 }}
className="inline-flex items-center rounded-full bg-[#0d7a57] px-8 py-3.5 text-sm font-semibold text-white shadow-[0_12px_28px_rgba(13,122,87,0.30)] transition-colors duration-200 hover:bg-[#095c41]"
>
Jetzt entscheiden
</motion.a>
<motion.a
href="#features"
whileHover={{ y: -2 }}
whileTap={{ scale: 0.97 }}
className="inline-flex items-center rounded-full border border-ink/20 bg-surface/80 px-8 py-3.5 text-sm font-semibold text-ink transition-colors duration-200 hover:border-[#0d7a57] hover:text-[#0d7a57]"
>
Mehr erfahren
</motion.a>
</div>
<p className="hero-el mt-8 inline-flex items-center rounded-full border border-[#0d7a57]/20 bg-[#e6f7f0] px-4 py-1.5 text-xs font-semibold uppercase tracking-[0.2em] text-[#0d7a57]">
<span className="inline-block h-1.5 w-1.5 rounded-full bg-[#0d7a57] mr-2.5" />
7 Tools · Kostenlos · Ohne Anmeldung
</p>
</div>
{/* Scroll-Hinweis */}
<a
href="#tools"
aria-label="Zu den Tools scrollen"
className="absolute bottom-6 left-1/2 z-10 hidden -translate-x-1/2 flex-col items-center gap-1 text-ink-soft transition-colors hover:text-ink md:flex"
>
<span className="text-[11px] font-semibold uppercase tracking-[0.2em]">
Scroll
</span>
<ArrowDown className="animate-bob h-4 w-4" />
</a>
</section>
);
}