since3-portal/modules/templates/catalog/blocks/accents.tsx
2026-10-11 18:22:32 +02:00

126 lines
5.2 KiB
TypeScript

"use client"
import {
motion,
useAnimationFrame,
useInView,
useMotionValue,
useReducedMotionConfig,
useScroll,
useSpring,
useTransform,
useVelocity,
wrap,
type MotionValue,
} from "motion/react"
import { useRef } from "react"
import { cn } from "@/lib/utils"
import { Reveal, wordmarkSize } from "../shared/motion"
import type { LaufbandData, WortmaskeData } from "./definitions"
import type { BlockProps } from "./types"
import { container, Eyebrow } from "./ui"
// ── Text-Laufband ────────────────────────────────────────────────────────────
// Eine Zeile: läuft gleichmäßig, schnelles Scrollen beschleunigt sie (auch rückwärts) und neigt die Schrift
function VelocityRow({ text, speed, boost, skew, outline, className }: {
text: string
speed: number
boost: MotionValue<number>
skew: MotionValue<number>
outline?: boolean
className?: string
}) {
const ref = useRef<HTMLDivElement>(null)
const base = useMotionValue(0)
const direction = useRef(1)
const reduce = useReducedMotionConfig()
const visible = useInView(ref)
const x = useTransform(base, (value) => `${wrap(-50, 0, value)}%`)
useAnimationFrame((_, delta) => {
if (reduce || !visible) return
const factor = boost.get()
if (factor < 0) direction.current = -1
else if (factor > 0) direction.current = 1
let move = direction.current * speed * (delta / 1000)
move += direction.current * move * factor
base.set(base.get() + move)
})
return (
<div ref={ref} className={cn("overflow-hidden", className)}>
<motion.div
style={{ x, skewX: skew }}
className={cn(
"flex w-max font-t-display text-[clamp(3rem,9vw,8.5rem)] leading-[1.05] tracking-[-0.03em] whitespace-nowrap",
outline && "text-transparent [-webkit-text-stroke:1.5px_var(--t-fg)]"
)}
>
{[0, 1].map((copy) => (
<span key={copy} aria-hidden={copy === 1 || undefined} className="flex shrink-0 items-center">
{Array.from({ length: 4 }, (_, i) => (
<span key={i} className="flex items-center pr-[0.4em]">
{text}
<span aria-hidden className="ml-[0.4em] text-[0.5em] text-t-accent [-webkit-text-stroke:0]">✦</span>
</span>
))}
</span>
))}
</motion.div>
</div>
)
}
export function Laufband({ data }: BlockProps<LaufbandData>) {
const { scrollY } = useScroll()
const velocity = useVelocity(scrollY)
const smooth = useSpring(velocity, { damping: 50, stiffness: 400 })
const boost = useTransform(smooth, [-2000, 0, 2000], [-5, 0, 5], { clamp: false })
const skew = useTransform(smooth, [-2500, 0, 2500], [7, 0, -7])
if (!data.text.trim()) return null
return (
<section className="border-y border-t-fg/10 py-8 md:py-12">
<VelocityRow text={data.text} speed={-2} boost={boost} skew={skew} />
{data.secondary.trim() && <VelocityRow text={data.secondary} speed={2} boost={boost} skew={skew} outline className="mt-1 md:mt-3" />}
</section>
)
}
// ── Bild im Schriftzug ───────────────────────────────────────────────────────
const IMAGE_SOURCE = /^(https?:\/\/|\/[^/])/
// Riesiges Wort, durch dessen Buchstaben ein Bild scheint; beim Scrollen wandert der Bildausschnitt mit
export function Wortmaske({ data, brand }: BlockProps<WortmaskeData>) {
const ref = useRef<HTMLElement>(null)
const { scrollYProgress } = useScroll({ target: ref, offset: ["start end", "end start"] })
const position = useTransform(scrollYProgress, [0, 1], ["50% 0%", "50% 100%"])
const word = data.word.trim() || brand.name
const image = IMAGE_SOURCE.test(data.image.trim())
? `url("${data.image.trim().replace(/["\\\n]/g, "")}")`
: "linear-gradient(135deg, var(--t-accent), var(--t-fg))"
return (
<section ref={ref} className="overflow-hidden py-20 md:py-32">
<div className={container}>
<Eyebrow>{data.eyebrow}</Eyebrow>
</div>
<motion.h2
style={{ backgroundImage: image, backgroundPosition: position, fontSize: wordmarkSize(word, 30) }}
className="mt-6 bg-cover bg-clip-text text-center font-t-display leading-[0.86] font-bold tracking-[-0.05em] whitespace-nowrap text-transparent [-webkit-background-clip:text]"
>
{word}
</motion.h2>
{data.text.trim() && (
<div className={cn(container, "mt-10 grid md:mt-14 md:grid-cols-12")}>
<Reveal className="md:col-span-5 md:col-start-7">
<p className="text-lg leading-relaxed text-t-muted">{data.text}</p>
</Reveal>
</div>
)}
</section>
)
}