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 (
)
}