/* Shared components for the prototype. Exposed on window for other babel scripts. */ const { useState, useEffect, useRef, useCallback } = React; /* ---- hooks ---- */ function useInView(opts) { const ref = useRef(null); const [seen, setSeen] = useState(false); useEffect(() => { const el = ref.current; if (!el) return; if (typeof IntersectionObserver === "undefined") { setSeen(true); return; } const io = new IntersectionObserver((entries) => { entries.forEach((e) => { if (e.isIntersecting) { setSeen(true); io.unobserve(e.target); } }); }, { threshold: 0.15, rootMargin: "0px 0px -8% 0px", ...(opts || {}) }); io.observe(el); return () => io.disconnect(); }, []); return [ref, seen]; } /* ---- Reveal: scroll-in wrapper ---- */ function Reveal({ children, delay, as, className = "", ...rest }) { const [ref, seen] = useInView(); const Tag = as || "div"; return ( {children} ); } /* ---- animated counter ---- */ function Counter({ to, suffix = "", text, dur = 1600 }) { const [ref, seen] = useInView({ threshold: 0.5 }); const [val, setVal] = useState(0); useEffect(() => { if (!seen || text) return; let raf, start; const step = (ts) => { if (!start) start = ts; const p = Math.min((ts - start) / dur, 1); const eased = 1 - Math.pow(1 - p, 3); setVal(Math.round(eased * to)); if (p < 1) raf = requestAnimationFrame(step); }; raf = requestAnimationFrame(step); return () => cancelAnimationFrame(raf); }, [seen, to, text, dur]); return ( {text ? text : <>{val}{suffix}} ); } /* ---- icons (minimal, geometric) ---- */ const Arrow = (p) => ( ); const Play = (p) => ( ); /* ---- image placeholder ---- */ function Ph({ motif, tone = "ph--sage", ratio, className = "", style }) { return (
Bild · {motif}
); } /* ---- section heading ---- */ function SecHead({ eyebrow, title, children, center, light, className = "" }) { return ( {eyebrow && {eyebrow}}

{title}

{children &&

{children}

}
); } const SunDivider = () => ( ); /* ---- Header ---- */ function Header({ route }) { const [open, setOpen] = useState(false); const [scrolled, setScrolled] = useState(false); useEffect(() => { const onScroll = () => setScrolled(window.scrollY > 12); onScroll(); window.addEventListener("scroll", onScroll, { passive: true }); return () => window.removeEventListener("scroll", onScroll); }, []); useEffect(() => { setOpen(false); }, [route]); const items = window.SITE.nav; const go = (e, href) => { setOpen(false); }; return (
Montessori Farm Aulendiebach Montessori Farm Aulendiebach Hospitieren
{items.map((n) => ( go(e, n.href)} className={route === n.id ? "active" : ""}>{n.label} ))} setOpen(false)}>Hospitieren
); } /* ---- Footer ---- */ function Footer() { const items = window.SITE.nav; const Social = ({ t }) => ( {t} ); return ( ); } /* ---- CTA band reused across pages ---- */ function CtaBand({ title, text, primary, secondary }) { return (

{title}

{text}

{primary.label} {secondary && {secondary.label}}
); } Object.assign(window, { useInView, Reveal, Counter, Arrow, Play, Ph, SecHead, SunDivider, Header, Footer, CtaBand, });