import { motion, type Variants } from 'motion/react' import type { ReactNode } from 'react' import { cn } from '../lib/cn' import { EASE_OUT_EXPO } from '../lib/motion' const tags = { h1: motion.h1, h2: motion.h2, h3: motion.h3, p: motion.p, div: motion.div } const lineVariants: Variants = { hidden: { y: '105%' }, show: { y: '0%', transition: { duration: 1.1, ease: EASE_OUT_EXPO } }, } /** Zeilen gleiten nacheinander aus einer Maske nach oben — beim Scrollen oder direkt beim Laden. */ export function RevealLines({ lines, as = 'h2', className, delay = 0, onLoad = false, srLabel, }: { lines: ReactNode[] as?: keyof typeof tags className?: string delay?: number onLoad?: boolean /** Text für Screenreader, wenn die sichtbaren Zeilen nur ein Ausschnitt sind */ srLabel?: string }) { const Tag = tags[as] const trigger = onLoad ? { animate: 'show' } : { whileInView: 'show', viewport: { once: true, amount: 0.5 } } return ( {srLabel && {srLabel}} {lines.map((line, i) => ( {line} ))} ) } /** Sanftes Einblenden beim Scrollen. */ export function FadeIn({ children, className, delay = 0 }: { children: ReactNode; className?: string; delay?: number }) { return ( {children} ) } /** Bild, das sich von unten aufdeckt und dabei leicht herauszoomt. */ export function ImageReveal({ src, alt, className, imgClassName, }: { src: string alt: string className?: string imgClassName?: string }) { const transition = { duration: 1.3, ease: EASE_OUT_EXPO } // Beobachtet wird der ungeclippte Rahmen: ein komplett weggeclipptes Element gilt nie als sichtbar return ( ) }