Skip to content
6

CTA 7

Closing CTA panel wrapped in a scroll-reactive warm rim glow that widens as the section moves through the viewport, with an eyebrow badge, a two-tone serif headline, sub-copy and dual CTAs. Static glow under reduced motion.

Preview

Installation

npx shadcn@latest add https://hirael.com/r/cta-07.json

Code

components/blocks/cta-07.tsx
'use client';

import * as React from 'react';
import { ArrowRight } from 'lucide-react';
import { motion, useReducedMotion, useScroll, useTransform } from 'motion/react';

import { cn } from '@/lib/utils';
import { Badge } from '@/registry/hirael/bases/radix/ui/badge';
import { Button } from '@/registry/hirael/bases/radix/ui/button';

const TITLE = 'Your next pipeline draws itself';

const glow = (w: number, h: number) =>
  `radial-gradient(${w}% ${h}% at 50% 0%, transparent 0%, transparent 55%, color-mix(in oklab, var(--primary) 30%, transparent) 82%, color-mix(in oklab, var(--primary) 30%, transparent) 100%)`;

// Resting state of the scroll-linked glow, used under reduced motion.
const STATIC_GLOW = glow(120, 130);

const Cta07 = () => {
  const reduced = useReducedMotion();
  const containerRef = React.useRef<HTMLDivElement>(null);
  const { scrollYProgress } = useScroll({
    target: containerRef,
    offset: ['end start', 'start end'],
  });

  const glowWidth = useTransform(scrollYProgress, [0, 1], [50, 140]);
  const glowHeight = useTransform(scrollYProgress, [0, 1], [70, 160]);
  const scrollGlow = useTransform([glowWidth, glowHeight], ([w, h]: Array<number>) =>
    glow(Math.max(Math.floor(w ?? 100), 100), Math.max(Math.round(h ?? 100), 100)),
  );
  const words = TITLE.split(' ');

  return (
    <div ref={containerRef} data-slot="cta" className="bg-background px-6 py-16 md:px-10 md:py-24">
      <div data-slot="cta-panel" className="relative mx-auto max-w-6xl overflow-hidden rounded-3xl px-6 pb-24 pt-18">
        <motion.div
          aria-hidden
          data-slot="cta-glow"
          style={{ background: reduced ? STATIC_GLOW : scrollGlow }}
          className="pointer-events-none absolute inset-0 opacity-50 dark:opacity-100"
        />
        <div
          data-slot="cta-body"
          className="relative z-10 mx-auto flex min-h-100 max-w-3xl flex-col items-center text-center"
        >
          <Badge
            variant="outline"
            className="rounded-full bg-card/70 px-4 py-1.5 font-mono text-[10px] uppercase tracking-[0.14em] text-muted-foreground"
          >
            Get started
          </Badge>

          <h2
            data-slot="cta-title"
            className="mt-4 font-serif text-balance text-3xl font-medium leading-[1.12] tracking-tight md:text-4xl lg:text-5xl"
          >
            {words.map((word, i) => (
              <span
                key={`${word}-${i}`}
                className={cn(
                  'me-2 inline-block',
                  i < Math.floor(words.length / 2) ? 'text-foreground' : 'text-muted-foreground',
                )}
              >
                {word}
              </span>
            ))}
          </h2>

          <p
            data-slot="cta-description"
            className="mt-4 max-w-md text-pretty text-base text-muted-foreground md:text-lg"
          >
            Import a repo, watch its workflow appear as a graph, and run it. The first pipeline takes minutes, not an
            afternoon.
          </p>

          <div data-slot="cta-actions" className="mt-8 flex w-full flex-col items-stretch gap-3 sm:w-auto sm:flex-row">
            <Button size="lg" asChild>
              <a href="#">
                Get started
                <ArrowRight aria-hidden className="rtl:-rotate-180" />
              </a>
            </Button>
            <Button size="lg" variant="outline" asChild>
              <a href="#">Read the docs</a>
            </Button>
          </div>
        </div>
      </div>
    </div>
  );
};

export default Cta07;

Dependencies

shadcn registry

badgebutton

npm

motionlucide-react

Shipped