Skip to content
6

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/bases/radix/ui/button';

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

const 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 oklab, var(--primary) 60%, transparent) 80%, color-mix(in oklab, 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' }}
          className="relative overflow-hidden rounded-3xl border border-border bg-card pt-16 pb-28 md:pt-20 md:pb-36"
        >
          <motion.div
            aria-hidden
            data-slot="cta-glow"
            style={{ background }}
            className="pointer-events-none absolute inset-0 opacity-60 dark:opacity-100"
          />
          <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>
  );
};

const 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>
  );
};

export default Cta05;

Dependencies

shadcn registry

button

npm

motionlucide-react