"use client"; /** * FINAL CTA — the closing convincer. Deliberately MORE dramatic than the hero: * - A big "cta" FluidBackground (brighter/faster cursor-reactive WebGL) fills * the whole section behind the content. * - KINETIC TYPE: the headline splits to chars that rise from a mask on view * (GSAP), and "grow?" keeps the brand gradient. Per-char pointer parallax — * each character leans toward the cursor with spring momentum, so the words * feel physically alive as you move across them. * - The primary button is magnetic (already) and gains an amplified glow here. * Reduced-motion / no-JS: type is fully visible and static; CSS gradient stands * in for the WebGL field. */ import { useEffect, useRef } from "react"; import Link from "next/link"; import { gsap, SplitText } from "./gsap"; import Magnetic from "./Magnetic"; import FluidBackground from "./FluidBackground"; import { SITE } from "../content"; export default function FinalCTA() { const root = useRef(null); const headRef = useRef(null); useEffect(() => { const el = root.current; const head = headRef.current; if (!el || !head) return; const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches; if (reduce) return; // Capture the gradient word's length BEFORE splitting — SplitText empties // the original , so we must read it first. const gradLen = (head.querySelector(".grad")?.textContent ?? "") .replace(/\s/g, "").length; const ctx = gsap.context(() => { const split = new SplitText(head, { type: "lines,chars", linesClass: "final__line", charsClass: "final__char", }); gsap.set(head, { autoAlpha: 1 }); const chars = split.chars as HTMLElement[]; // Re-establish the brand gradient on the chars of the gradient word. // After SplitText restructures into lines, the original .grad span is // emptied and its chars become plain divs that inherit // `-webkit-text-fill-color:transparent` WITHOUT a background (=> invisible). // The gradient word is the trailing word, so the last `gradLen` chars get // the .final__char--grad class which repaints each glyph with the gradient. if (gradLen > 0) { chars.slice(chars.length - gradLen).forEach((c) => { c.classList.add("final__char--grad"); }); } // chars rise from behind the line mask on view; the trigger fires as the // section enters. `once` + the natural (visible) resting state guarantee // the headline ends fully shown. gsap.from(chars, { yPercent: 120, opacity: 0, duration: 0.7, ease: "emilOut", stagger: 0.02, scrollTrigger: { trigger: el, start: "top 85%", once: true }, }); // per-char pointer parallax: each char leans toward the cursor. const qx = chars.map((c) => gsap.quickTo(c, "x", { duration: 0.6, ease: "power3.out" })); const qy = chars.map((c) => gsap.quickTo(c, "y", { duration: 0.6, ease: "power3.out" })); const onMove = (e: PointerEvent) => { const cx = e.clientX; const cy = e.clientY; chars.forEach((c, i) => { const cr = c.getBoundingClientRect(); const dx = cx - (cr.left + cr.width / 2); const dy = cy - (cr.top + cr.height / 2); const dist = Math.hypot(dx, dy); const pull = Math.max(0, 1 - dist / 420); qx[i](dx * 0.06 * pull); qy[i](dy * 0.06 * pull); }); }; const onLeave = () => chars.forEach((_, i) => { qx[i](0); qy[i](0); }); el.addEventListener("pointermove", onMove); el.addEventListener("pointerleave", onLeave); return () => { el.removeEventListener("pointermove", onMove); el.removeEventListener("pointerleave", onLeave); }; }, el); return () => ctx.revert(); }, []); return (
{/* dramatic cursor-reactive WebGL field — the closing moment */}

The bottom line

Ready to grow?

No long contracts. No vanity reports. Marketing you can measure in sales.

Book a call or {SITE.email}
); }