De afsluitende oproep van een pagina: een reusachtige, bijna onzichtbare lopende band met één zin erachter, en daarvoor gecentreerd een label, de kop, een toelichting, de knop en een voetnoot. Receptuur: marquee + knop-slot.
blocks/cta/cta-marquee.tsx
/**
* CtaMarquee — de afsluitende oproep van een pagina: een reusachtige,
* bijna onzichtbare lopende band met één zin erachter, en daarvoor gecentreerd
* een label, de kop, een toelichting, de knop en een voetnoot.
*
* Receptuur: de band is de bestaande marquee (FC-0029), de knop komt als slot
* (meestal button FC-0003). Alle tekst is van het project; de band herhaalt
* de zin acht keer zodat één helft altijd breder is dan het scherm — anders
* trekt er een gat door het beeld voor de lus opnieuw begint.
*/
"use client";
import type * as React from "react";
import { Marquee } from "../../primitives/ui/marquee";
export interface CtaMarqueeProps extends Omit<React.HTMLAttributes<HTMLElement>, "children"> {
/** De zin die op de achtergrond voorbijtrekt. */
phrase: string;
/** Klein label boven de kop. */
badge?: string;
heading: React.ReactNode;
/** Korte toelichting onder de kop. */
note?: React.ReactNode;
/** De knop (of knoppen) — meestal een Button. */
action?: React.ReactNode;
/** Kleine regel onder de knop. */
footnote?: React.ReactNode;
/** Seconden per doorloop van de band. */
duration?: number;
}
const HERHALINGEN = 8;
export function CtaMarquee({ phrase, badge, heading, note, action, footnote, duration = 40, className, ...rest }: CtaMarqueeProps) {
const regel = `${phrase} · `.repeat(HERHALINGEN);
return (
<section {...rest} className={["fp-ctam", className].filter(Boolean).join(" ")}>
<div className="fp-ctam__band" aria-hidden="true">
<Marquee duration={duration} fade={false} gap="0">
<span className="fp-ctam__zin">{regel}</span>
</Marquee>
</div>
<div className="fp-ctam__inhoud">
{badge && <span className="fp-ctam__label">{badge}</span>}
<h2 className="fp-ctam__kop">{heading}</h2>
{note && <p className="fp-ctam__noot">{note}</p>}
{action && <div className="fp-ctam__actie">{action}</div>}
{footnote && <p className="fp-ctam__voetnoot">{footnote}</p>}
</div>
</section>
);
}
blocks/cta/cta.css
/**
* CTA-blocks: cta-marquee.
* Importeren in de app-root (de registry-site doet dat in app/globals.css).
*/
/* ── CtaMarquee ────────────────────────────────────────────────────────── */
.fp-ctam {
position: relative; overflow: hidden; width: 100%;
padding: 4rem 1rem;
background: var(--background); color: var(--foreground);
}
@media (min-width: 48rem) { .fp-ctam { padding: 6rem 1.5rem; } }
/* De band: één reusachtige regel, bijna onzichtbaar, achter alles. */
.fp-ctam__band {
position: absolute; inset: 0; display: flex; align-items: center;
pointer-events: none; user-select: none;
color: color-mix(in srgb, var(--foreground) 6%, transparent);
}
.fp-ctam__zin {
display: inline-block; white-space: nowrap;
font: inherit; font-weight: 700; font-size: 22vw; line-height: 1; letter-spacing: -0.04em;
}
@media (min-width: 48rem) { .fp-ctam__zin { font-size: 16vw; } }
.fp-ctam__inhoud {
position: relative; display: flex; flex-direction: column; align-items: center;
max-width: 48rem; margin: 0 auto; text-align: center;
}
.fp-ctam__label {
display: inline-flex; align-items: center; padding: 0.375rem 0.875rem;
font-size: 0.75rem; font-weight: 500; letter-spacing: 0.08em; text-transform: uppercase;
color: var(--muted-foreground); border: 1px solid var(--border); border-radius: 999px;
}
.fp-ctam__kop {
margin: 2rem 0 0; font: inherit; font-weight: 700; letter-spacing: -0.02em; line-height: 1.05;
font-size: clamp(2.25rem, 6vw, 4.5rem); text-wrap: balance;
}
.fp-ctam__noot { margin: 1.5rem 0 0; max-width: 36rem; font-size: 1.125rem; line-height: 1.5; color: var(--muted-foreground); text-wrap: balance; }
@media (min-width: 48rem) { .fp-ctam__noot { font-size: 1.25rem; } }
.fp-ctam__actie { display: flex; flex-wrap: wrap; gap: 0.75rem; justify-content: center; margin-top: 3rem; }
.fp-ctam__voetnoot { margin: 2rem 0 0; font-size: 1rem; color: var(--muted-foreground); }