Command Palette

Search for a command to run...

Docs
Breeze Waitlist

Breeze Waitlist

A minimal, animated waitlist form component with Framer Motion transitions.

Loading...

Installation

Install the following dependencies:

npm install clsx tailwind-merge zod lucide-react canvas-confetti framer-motion

Add the utils.ts file to the @/lib folder

utils.ts

import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
 
export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

Create a breeze-waitlist.tsx file and copy the below code.

breeze-waitlist.tsx

"use client";
 
import React, { useState, RefObject, useEffect, useRef } from "react";
import { z } from "zod";
import { Mail, X } from "lucide-react";
import confetti from "canvas-confetti";
import { AnimatePresence, motion } from "framer-motion";
 
// Email validation schema
const emailSchema = z.object({
  email: z.string().email({ message: "Invalid email address" }),
});
 
// Confetti animation
const launchConfetti = () => {
  const duration = 4 * 1000;
  const animationEnd = Date.now() + duration;
  let skew = 1;
 
  const randomInRange = (min: number, max: number) =>
    Math.random() * (max - min) + min;
 
  (function frame() {
    const timeLeft = animationEnd - Date.now();
    const ticks = Math.max(200, 500 * (timeLeft / duration));
    skew = Math.max(0.8, skew - 0.001);
    confetti({
      particleCount: 1,
      startVelocity: 0,
      ticks,
      origin: {
        x: Math.random(),
        y: Math.random() * skew - 0.2,
      },
      colors: ["#ffffff"],
      shapes: ["circle"],
      gravity: randomInRange(0.4, 0.6),
      scalar: randomInRange(0.4, 1),
      drift: randomInRange(-0.4, 0.4),
    });
 
    if (timeLeft > 0) {
      requestAnimationFrame(frame);
    }
  })();
};
 
// Detect outside click
const useClickOutside = (
  ref: RefObject<HTMLElement | null>,
  handleOnClickOutside: (event: MouseEvent | TouchEvent) => void
) => {
  useEffect(() => {
    const listener = (event: MouseEvent | TouchEvent) => {
      if (!ref.current || ref.current.contains(event.target as Node)) {
        return;
      }
      handleOnClickOutside(event);
    };
    document.addEventListener("mousedown", listener);
    document.addEventListener("touchstart", listener);
    return () => {
      document.removeEventListener("mousedown", listener);
      document.removeEventListener("touchstart", listener);
    };
  }, [ref, handleOnClickOutside]);
};
 
const BreezeWaitlist = () => {
  const [formData, setFormData] = useState({ email: "" });
  const [error, setError] = useState<string | null>(null);
  const [submittedEmail, setSubmittedEmail] = useState("");
  const [open, setOpen] = useState(false);
  const [submitting, setSubmitting] = useState(false);
  const [success, setSuccess] = useState(false);
 
  // Email validation
  const validate = (email: string) => {
    const result = emailSchema.safeParse({ email });
    setError(result.success ? null : result.error.issues[0].message);
    return result.success;
  };
 
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setFormData({ email: e.target.value });
    validate(e.target.value);
  };
 
  // Handle form submission locally
  const handleFormSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    if (validate(formData.email)) {
      setSubmitting(true);
      setTimeout(() => {
        setSubmittedEmail(formData.email);
        setSuccess(true);
        launchConfetti();
        setSubmitting(false);
      }, 1000); // simulate async delay
    }
  };
 
  const ref = useRef<HTMLDivElement>(null);
  useClickOutside(ref, () => setOpen(false));
 
  return (
    <>
      {/* Trigger Button */}
      <motion.button
        layoutId="waitlist-wrapper"
        onClick={() => setOpen(true)}
        className="group inline-flex items-center gap-2 rounded-xl bg-black text-white font-semibold px-6 py-3 hover:bg-black/60 transition cursor-pointer"
      >
        Get Early Access
        <motion.span
          layoutId="waitlist-title"
          className="group-hover:translate-x-1 transition-transform"
        >
          👀
        </motion.span>
      </motion.button>
 
      {/* Modal */}
      <AnimatePresence>
        {open && (
          <motion.div
            key="modal"
            className="fixed inset-0 z-50 flex items-center justify-center px-4"
            layoutId="waitlist-wrapper"
          >
            <div className="bg-amber-100/20 p-1 rounded-2xl">
              <div
                ref={ref}
                className="bg-black/75 backdrop-blur-md w-full md:min-w-md p-6 md:px-10 rounded-2xl relative shadow-lg"
              >
                <button
                  onClick={() => setOpen(false)}
                  className="absolute top-4 right-4 text-white hover:bg-black rounded-full p-1 cursor-pointer"
                >
                  <X size={20} />
                </button>
                <motion.span
                  aria-hidden
                  className="text-sm text-transparent"
                  layoutId="waitlist-title"
                >
                  Get Early Access 👀
                </motion.span>
 
                <AnimatePresence mode="popLayout">
                  {success ? (
                    <motion.div
                      key="success"
                      initial={{ y: -32, opacity: 0, filter: "blur(4px)" }}
                      animate={{ y: 0, opacity: 1, filter: "blur(0px)" }}
                      transition={{ type: "spring", duration: 0.4, bounce: 0 }}
                    >
                      <div className="text-center space-y-2 p-10">
                        <p className="text-xl text-gray-100">You're in!</p>
                        <p className="text-xl text-gray-100">
                          We'll notify you early.
                        </p>
                      </div>
                    </motion.div>
                  ) : (
                    <motion.div
                      exit={{ y: 8, opacity: 0, filter: "blur(4px)" }}
                      transition={{ type: "spring", duration: 0.4, bounce: 0 }}
                      key="open-child"
                    >
                      <form
                        onSubmit={handleFormSubmit}
                        className="flex flex-col items-start py-12"
                      >
                        <div className="flex gap-2 w-full">
                          <div className="w-full relative">
                            <div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
                              <Mail className="text-muted-foreground size-4" />
                            </div>
                            <input
                              type="email"
                              required
                              placeholder="Enter your email"
                              className="pl-10 w-full px-4 py-3 text-muted-foreground rounded-xl border border-amber-100/20 focus:outline-none focus:ring-white/20 focus:border-white/20"
                              value={formData.email}
                              onChange={handleChange}
                            />
                          </div>
                          <button
                            type="submit"
                            disabled={submitting}
                            className="group inline-flex items-center gap-2 rounded-xl bg-gradient-to-b from-white to-white/50 text-black font-semibold px-4 py-3 hover:bg-white transition cursor-pointer"
                          >
                            {submitting ? "Submitting..." : "Submit"}
                          </button>
                        </div>
                        {error && (
                          <p className="text-red-400 text-sm pl-2 pt-1">
                            {error}
                          </p>
                        )}
                      </form>
                    </motion.div>
                  )}
                </AnimatePresence>
              </div>
            </div>
          </motion.div>
        )}
      </AnimatePresence>
    </>
  );
};
 
export default BreezeWaitlist;

Update the import paths to match your project setup.

Usage

import BreezeWaitlist from "@/components/breezeblocks/breeze-waitlist";
<div className="__waitlist">
  <BreezeWaitlist />
</div>

Props

PropTypeDescriptionDefault
This component takes no props.