# CTA 2: waitlist over light beams

> A waitlist or newsletter signup over rising light beams: heading, email field with validation, loading, success and error states.

A shadcn/ui block from the [@ballmac](https://ui.ballmac.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/ballmac/cta-2).

- Page, with a live preview: https://shadcn.exchange/r/ballmac/cta-2
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/ballmac/cta-2-light-caaffdd14634fc1d.png
- Registry item: https://ui.ballmac.com/r/cta-2.json
- Categories: blocks, cta
- Listed: 2026-10-14, source last changed 2026-10-14

## Install

```bash
npx shadcn@latest add https://ui.ballmac.com/r/cta-2.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `lucide-react`
- Registry items: `utils`, `https://ui.ballmac.com/r/beams-background.json`, `https://ui.ballmac.com/r/button.json`, `https://ui.ballmac.com/r/input.json`

## Files

### `registry/ballmac/components/blocks/cta-2/cta-2.tsx`

```tsx
// Ballmac UI: CTA 2. https://ui.ballmac.com/blocks/cta-2
"use client"

import * as React from "react"
import { Check } from "lucide-react"

import { BeamsBackground } from "@/components/ballmac/beams-background"
import { Button } from "@/components/ballmac/button"
import { Input } from "@/components/ballmac/input"
import { cn } from "@/lib/utils"

type Cta2Props = Omit<React.ComponentProps<"section">, "title" | "onSubmit"> & {
  /** Small label above the heading. */
  eyebrow?: string
  /** The heading. */
  title?: string
  /** One sentence under the heading. */
  description?: string
  /** Submit button text. */
  buttonLabel?: string
  /** Line under the form, e.g. social proof. */
  note?: string
  /** Message shown after a successful signup. */
  successMessage?: string
  /** Called with the email. Throw to show an error; resolve to show the success message. */
  onSubmit?: (email: string) => void | Promise<void>
}

function Cta2({
  eyebrow = "Private beta",
  title = "Join the waitlist.",
  description = "We let new teams in every week. Leave your email and we'll save you a seat.",
  buttonLabel = "Request access",
  note = "No spam. One email when your seat is ready.",
  successMessage = "You're on the list. We'll email you when your seat is ready.",
  onSubmit,
  className,
  ...props
}: Cta2Props) {
  const id = React.useId()
  const [state, setState] = React.useState<"idle" | "sending" | "done" | "error">("idle")
  const submit = async (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault()
    const email = String(new FormData(event.currentTarget).get("email") ?? "")
    setState("sending")
    try {
      await onSubmit?.(email)
      setState("done")
    } catch {
      setState("error")
    }
  }
  return (
    <section data-slot="cta-2" className={cn("mx-auto max-w-6xl px-4 py-20 sm:px-6", className)} {...props}>
      <div className="relative isolate flex flex-col items-center overflow-hidden rounded-3xl border bg-background px-6 py-24 text-center sm:py-28">
        <BeamsBackground className="-z-10" />
        <div aria-hidden="true" className="pointer-events-none absolute inset-0 -z-10 bg-[radial-gradient(ellipse_45%_45%_at_50%_50%,var(--background)_25%,transparent)]" />
        <p className="font-mono text-xs tracking-widest text-muted-foreground uppercase">{eyebrow}</p>
        <h2 className="mt-3 max-w-xl text-4xl font-semibold tracking-[-0.04em] text-balance sm:text-6xl">{title}</h2>
        <p className="mt-4 max-w-md text-pretty text-muted-foreground">{description}</p>
        {state === "done" ? (
          <p role="status" className="mt-8 inline-flex items-center gap-2 rounded-full border bg-background/80 px-4 py-2 text-sm font-medium backdrop-blur">
            <Check className="size-4 text-chart-2" aria-hidden="true" />
            {successMessage}
          </p>
        ) : (
          <form onSubmit={submit} className="mt-8 flex w-full max-w-md flex-col gap-2 sm:flex-row">
            <label htmlFor={`${id}-email`} className="sr-only">
              Email address
            </label>
            <Input
              id={`${id}-email`}
              name="email"
              type="email"
              required
              autoComplete="email"
              placeholder="you@company.com"
              aria-invalid={state === "error" || undefined}
              aria-describedby={state === "error" ? `${id}-error` : undefined}
              className="h-10 bg-background/70 backdrop-blur"
            />
            <Button type="submit" loading={state === "sending"} className="h-10 shrink-0">
              {buttonLabel}
            </Button>
          </form>
        )}
        {state === "error" ? (
          <p id={`${id}-error`} role="alert" className="mt-3 text-sm text-destructive">
            Something went wrong. Please try again.
          </p>
        ) : (
          state !== "done" && <p className="mt-4 text-xs text-muted-foreground">{note}</p>
        )}
      </div>
    </section>
  )
}

export { Cta2, type Cta2Props }
```
