CTA 5

Contact CTA panel with a scroll-reactive radial glow, an eyebrow badge, a word-by-word reveal headline, and two contact actions (email and call) each pairing a button with its detail line.

Preview

Installation

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

Code

components/blocks/cta-05.tsx
"use client";

import * as React from "react";
import { Mail, Phone } from "lucide-react";
import { motion, useScroll, useTransform } from "motion/react";

import { cn } from "@/lib/utils";
import { Button } from "@/registry/hirael/ui/button";

const email = "team@example.com";
const phone = "+1 (555) 012 3456";

export default function Cta05() {
  const containerRef = React.useRef<HTMLDivElement>(null);
  const { scrollYProgress } = useScroll({
    target: containerRef,
    offset: ["end start", "start end"],
  });

  const gradientWidth = useTransform(scrollYProgress, [0, 1], [50, 140]);
  const gradientHeight = useTransform(scrollYProgress, [0, 1], [70, 160]);
  const background = useTransform([gradientWidth, gradientHeight], (values) => {
    const [width, height] = values as number[];
    const w = Math.max(Math.floor(width), 100);
    const h = Math.max(Math.round(height), 100);
    return `radial-gradient(${w}% ${h}% at 50% 0%, transparent 0%, transparent 55%, color-mix(in oklch, var(--primary) 60%, transparent) 80%, color-mix(in oklch, var(--primary) 18%, var(--card)) 100%)`;
  });

  return (
    <section data-slot="cta" className="bg-background py-12 md:py-16">
      <div ref={containerRef} className="container w-full">
        <motion.div
          data-slot="cta-panel"
          initial={{ opacity: 0, y: 20 }}
          whileInView={{ opacity: 1, y: 0 }}
          viewport={{ once: true }}
          transition={{ duration: 0.8, ease: "easeOut" }}
          style={{ background }}
          className="relative overflow-hidden rounded-3xl border border-border bg-card pt-16 pb-28 md:pt-20 md:pb-36"
        >
          <div className="relative z-10 mx-auto flex max-w-3xl flex-col items-center px-6 text-center">
            <motion.span
              data-slot="cta-badge"
              initial={{ opacity: 0, scale: 0.9 }}
              whileInView={{ opacity: 1, scale: 1 }}
              viewport={{ once: true }}
              transition={{ duration: 0.6, ease: "easeOut", delay: 0.2 }}
              className="inline-flex w-fit items-center gap-2 rounded-full border border-border bg-background/60 px-4 py-1.5 font-mono text-[11px] uppercase tracking-[0.14em] text-muted-foreground backdrop-blur-sm"
            >
              <span className="size-1.5 rounded-full bg-primary" />
              Get in touch
            </motion.span>

            <AnimatedTitle
              data-slot="cta-title"
              text="Let's talk about your next project"
              className="mt-5"
            />

            <motion.p
              data-slot="cta-copy"
              initial={{ opacity: 0, y: 20 }}
              whileInView={{ opacity: 1, y: 0 }}
              viewport={{ once: true }}
              transition={{ duration: 0.6, delay: 0.4 }}
              className="mt-4 max-w-2xl text-base leading-relaxed text-muted-foreground md:text-lg"
            >
              Tell us what you’re building and where it’s stuck. We read every
              message and usually reply within a day.
            </motion.p>

            <motion.div
              data-slot="cta-actions"
              initial={{ opacity: 0, y: 20 }}
              whileInView={{ opacity: 1, y: 0 }}
              viewport={{ once: true }}
              transition={{ duration: 0.5, delay: 0.6 }}
              className="mt-10 grid w-full max-w-md grid-cols-1 gap-5 md:grid-cols-2"
            >
              <div className="flex flex-col items-center gap-2">
                <Button asChild size="lg" className="w-full rounded-full">
                  <a href={`mailto:${email}`}>
                    <Mail className="size-4" />
                    Send email
                  </a>
                </Button>
                <p className="text-sm font-medium text-muted-foreground">
                  {email}
                </p>
              </div>
              <div className="flex flex-col items-center gap-2">
                <Button
                  asChild
                  size="lg"
                  variant="outline"
                  className="w-full rounded-full"
                >
                  <a href={`tel:${phone.replace(/[^\d+]/g, "")}`}>
                    <Phone className="size-4" />
                    Call us
                  </a>
                </Button>
                <p className="text-sm font-medium text-muted-foreground">
                  {phone}
                </p>
              </div>
            </motion.div>
          </div>
        </motion.div>
      </div>
    </section>
  );
}

function AnimatedTitle({
  text,
  className,
  ...props
}: React.ComponentProps<"h2"> & { text: string }) {
  const words = text.split(" ");

  return (
    <h2
      className={cn(
        "mx-auto max-w-3xl font-serif text-3xl font-medium leading-tight tracking-tight text-foreground md:text-5xl",
        className,
      )}
      {...props}
    >
      {words.map((word, i) => (
        <motion.span
          key={i}
          initial={{ opacity: 0, y: 20 }}
          whileInView={{ opacity: 1, y: 0 }}
          viewport={{ once: true }}
          transition={{ duration: 0.5, ease: "easeOut", delay: i * 0.08 }}
          className="me-2 inline-block"
        >
          {word}
        </motion.span>
      ))}
    </h2>
  );
}

Dependencies

shadcn registry

button

npm

motionlucide-react