Fazekit

Code

"use client";
import * as React from "react";
import { motion, useInView, useReducedMotion } from "motion/react";
import { ArrowRight, Check } from "lucide-react";
import { cn } from "@/lib/utils";

export interface CtaGradientBorderProps {
  eyebrow?: string;
  headline?: string;
  description?: string;
  primaryCta?: { label: string; href?: string };
  secondaryCta?: { label: string; href?: string };
  /** Small reassurance points under the buttons. */
  reassurance?: string[];
  /** Seconds for one full rotation of the border. */
  duration?: number;
  className?: string;
}

const EASE = [0.22, 1, 0.36, 1] as const;

export function CtaGradientBorder({
  eyebrow = "Ready when you are",
  headline = "Start building your next launch today",
  description = "Join 18,000 teams shipping faster with Acme. Set up in minutes, invite your team, and see results before your first coffee gets cold.",
  primaryCta = { label: "Start for free", href: "#" },
  secondaryCta = { label: "Talk to sales", href: "#" },
  reassurance = ["No credit card required", "14-day Pro trial", "Cancel anytime"],
  duration = 6,
  className,
}: CtaGradientBorderProps) {
  const reduce = useReducedMotion();
  const ref = React.useRef<HTMLDivElement>(null);
  const inView = useInView(ref, { once: true, amount: 0.3 });

  const conic =
    "conic-gradient(from 0deg, transparent 0deg, #8b5cf6 60deg, #ec4899 110deg, #f59e0b 150deg, transparent 200deg, transparent 360deg)";

  return (
    <section className={cn("relative w-full overflow-hidden bg-background px-5 py-16 text-foreground sm:px-8 sm:py-20", className)}>
      <div aria-hidden className="pointer-events-none absolute inset-0 bg-[radial-gradient(var(--color-border)_1px,transparent_1px)] bg-[size:20px_20px] [mask-image:radial-gradient(ellipse_60%_60%_at_50%_50%,#000,transparent)]" />

      <motion.div
        ref={ref}
        initial={reduce ? { opacity: 0 } : { opacity: 0, y: 30, scale: 0.97 }}
        animate={inView ? { opacity: 1, y: 0, scale: 1 } : undefined}
        transition={{ duration: 0.8, ease: EASE }}
        className="relative mx-auto max-w-4xl"
      >
        {/* Soft glow that follows the border */}
        <div aria-hidden className="absolute -inset-px overflow-hidden rounded-[28px] opacity-60 blur-2xl">
          <motion.div
            className="absolute inset-[-100%]"
            style={{ background: conic }}
            animate={reduce ? undefined : { rotate: 360 }}
            transition={{ duration, repeat: Infinity, ease: "linear" }}
          />
        </div>

        {/* Border ring */}
        <div className="relative overflow-hidden rounded-[28px] p-px">
          <div aria-hidden className="absolute inset-0 bg-border" />
          <motion.div
            aria-hidden
            className="absolute inset-[-100%]"
            style={{ background: conic }}
            animate={reduce ? undefined : { rotate: 360 }}
            transition={{ duration, repeat: Infinity, ease: "linear" }}
          />

          <div className="relative overflow-hidden rounded-[27px] bg-card px-6 py-14 text-center sm:px-14 sm:py-16">
            <div aria-hidden className="pointer-events-none absolute -top-24 left-1/2 h-48 w-[80%] -translate-x-1/2 rounded-full bg-fuchsia-500/10 blur-3xl dark:bg-fuchsia-500/15" />
            <p className="relative inline-flex items-center gap-2 rounded-full border bg-background/60 px-3 py-1 text-xs font-medium text-muted-foreground">
              <span className="size-1.5 rounded-full bg-linear-to-r from-violet-500 to-pink-500" />
              {eyebrow}
            </p>
            <h2 className="relative mx-auto mt-6 max-w-2xl text-balance text-3xl font-semibold tracking-tight sm:text-5xl">{headline}</h2>
            <p className="relative mx-auto mt-5 max-w-xl text-pretty leading-relaxed text-muted-foreground">{description}</p>

            <div className="relative mt-9 flex flex-col items-center justify-center gap-3 sm:flex-row">
              <a
                href={primaryCta.href ?? "#"}
                className="group relative inline-flex h-12 w-full items-center justify-center gap-2 overflow-hidden rounded-full bg-foreground px-7 text-sm font-semibold text-background shadow-lg transition hover:opacity-95 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-card sm:w-auto"
              >
                <span aria-hidden className="absolute inset-y-0 -left-1/2 w-1/2 -skew-x-12 bg-linear-to-r from-transparent via-white/25 to-transparent transition-transform duration-700 group-hover:translate-x-[300%] dark:via-black/15" />
                {primaryCta.label}
                <ArrowRight className="size-4 transition-transform group-hover:translate-x-0.5" aria-hidden />
              </a>
              <a
                href={secondaryCta.href ?? "#"}
                className="inline-flex h-12 w-full items-center justify-center rounded-full border bg-background px-7 text-sm font-semibold transition hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring sm:w-auto"
              >
                {secondaryCta.label}
              </a>
            </div>

            {reassurance.length > 0 && (
              <ul className="relative mt-7 flex flex-wrap items-center justify-center gap-x-5 gap-y-2 text-xs text-muted-foreground sm:text-sm">
                {reassurance.map((r) => (
                  <li key={r} className="flex items-center gap-1.5">
                    <Check className="size-3.5 text-emerald-600 dark:text-emerald-400" aria-hidden />
                    {r}
                  </li>
                ))}
              </ul>
            )}
          </div>
        </div>
      </motion.div>
    </section>
  );
}

More in Call to Action

View all →