Web InnoventixFreeCode

Modal Newsletter

Original · free

newsletter popup card

byWeb InnoventixReact + Tailwind
newsxmodalnewsletter
Open

Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.

npx shadcn@latest add https://webinnoventix.com/r/newsx-modal.json
newsx-modal.tsx
"use client";

import {
  useCallback,
  useEffect,
  useId,
  useMemo,
  useRef,
  useState,
} from "react";
import type {
  ChangeEvent,
  FormEvent,
  KeyboardEvent as ReactKeyboardEvent,
  MouseEvent as ReactMouseEvent,
} from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";

type Status = "idle" | "submitting" | "success";

type Topic = {
  readonly id: string;
  readonly label: string;
  readonly hint: string;
};

const TOPICS: readonly Topic[] = [
  { id: "shipping", label: "Shipping notes", hint: "What we launched, honestly" },
  { id: "engineering", label: "Engineering", hint: "Deep-dives, no fluff" },
  { id: "craft", label: "Design & craft", hint: "Details worth stealing" },
  { id: "field", label: "Field reports", hint: "Real teams, real numbers" },
];

const SUBSCRIBERS = [
  { initials: "AR", tone: "bg-indigo-500" },
  { initials: "MJ", tone: "bg-violet-500" },
  { initials: "KP", tone: "bg-emerald-500" },
  { initials: "TS", tone: "bg-amber-500" },
] as const;

const FOCUSABLE =
  'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';

function isValidEmail(value: string): boolean {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value.trim());
}

export default function NewsxModal() {
  const reduce = useReducedMotion();

  const [open, setOpen] = useState(false);
  const [email, setEmail] = useState("");
  const [touched, setTouched] = useState(false);
  const [topics, setTopics] = useState<readonly string[]>(["shipping"]);
  const [status, setStatus] = useState<Status>("idle");

  const dialogRef = useRef<HTMLDivElement | null>(null);
  const emailRef = useRef<HTMLInputElement | null>(null);
  const openerRef = useRef<HTMLElement | null>(null);
  const timerRef = useRef<number | null>(null);

  const uid = useId();
  const titleId = `${uid}-title`;
  const descId = `${uid}-desc`;
  const errId = `${uid}-err`;
  const topicsLabelId = `${uid}-topics`;

  const emailInvalid = touched && !isValidEmail(email);

  const openModal = useCallback((trigger?: HTMLElement | null) => {
    openerRef.current =
      trigger ?? (document.activeElement as HTMLElement | null);
    setOpen(true);
  }, []);

  const closeModal = useCallback(() => {
    setOpen(false);
  }, []);

  // Auto-surface once, like a real popup — but never fight reduced-motion users abruptly.
  useEffect(() => {
    const t = window.setTimeout(() => {
      setOpen((prev) => prev || true);
    }, 900);
    return () => window.clearTimeout(t);
  }, []);

  // Body scroll lock while open.
  useEffect(() => {
    if (!open) return;
    const prev = document.body.style.overflow;
    document.body.style.overflow = "hidden";
    return () => {
      document.body.style.overflow = prev;
    };
  }, [open]);

  // Focus management: move focus in on open, restore it on close.
  useEffect(() => {
    if (open) {
      const raf = window.requestAnimationFrame(() => {
        emailRef.current?.focus();
      });
      return () => window.cancelAnimationFrame(raf);
    }
    const opener = openerRef.current;
    if (opener && typeof opener.focus === "function") {
      opener.focus();
    }
    // Reset transient form state after the exit animation would have run.
    const t = window.setTimeout(() => {
      setStatus("idle");
      setTouched(false);
    }, 260);
    return () => window.clearTimeout(t);
  }, [open]);

  // Re-trap focus if the browser drops it outside the dialog — the submit
  // button blurs to <body> the instant it becomes disabled, and from there the
  // dialog's key handler can't fire, so Tab would reach the page behind it.
  useEffect(() => {
    if (!open || status === "idle") return;
    const root = dialogRef.current;
    if (!root) return;
    const active = document.activeElement as HTMLElement | null;
    if (active && root.contains(active)) return;
    root.querySelector<HTMLElement>(FOCUSABLE)?.focus();
  }, [open, status]);

  useEffect(() => {
    return () => {
      if (timerRef.current !== null) window.clearTimeout(timerRef.current);
    };
  }, []);

  const onKeyDown = useCallback(
    (event: ReactKeyboardEvent<HTMLDivElement>) => {
      if (event.key === "Escape") {
        event.stopPropagation();
        closeModal();
        return;
      }
      if (event.key !== "Tab") return;

      const root = dialogRef.current;
      if (!root) return;
      const nodes = Array.from(
        root.querySelectorAll<HTMLElement>(FOCUSABLE),
      ).filter((el) => el.offsetParent !== null || el === document.activeElement);
      if (nodes.length === 0) return;

      const first = nodes[0];
      const last = nodes[nodes.length - 1];
      const active = document.activeElement as HTMLElement | null;

      if (event.shiftKey && active === first) {
        event.preventDefault();
        last.focus();
      } else if (!event.shiftKey && active === last) {
        event.preventDefault();
        first.focus();
      }
    },
    [closeModal],
  );

  const toggleTopic = useCallback((id: string) => {
    setTopics((prev) =>
      prev.includes(id) ? prev.filter((t) => t !== id) : [...prev, id],
    );
  }, []);

  const onSubmit = useCallback(
    (event: FormEvent<HTMLFormElement>) => {
      event.preventDefault();
      setTouched(true);
      if (!isValidEmail(email) || status === "submitting") return;
      setStatus("submitting");
      timerRef.current = window.setTimeout(() => {
        setStatus("success");
      }, 1100);
    },
    [email, status],
  );

  const onEmailChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
    setEmail(event.target.value);
  }, []);

  const onBackdrop = useCallback(
    (event: ReactMouseEvent<HTMLDivElement>) => {
      if (event.target === event.currentTarget) closeModal();
    },
    [closeModal],
  );

  const dur = reduce ? 0 : 0.32;
  const backdropVariants = useMemo(
    () => ({
      hidden: { opacity: 0 },
      show: { opacity: 1 },
    }),
    [],
  );
  const cardVariants = useMemo(
    () => ({
      hidden: reduce
        ? { opacity: 0 }
        : { opacity: 0, y: 22, scale: 0.965 },
      show: reduce
        ? { opacity: 1 }
        : { opacity: 1, y: 0, scale: 1 },
      exit: reduce
        ? { opacity: 0 }
        : { opacity: 0, y: 14, scale: 0.975 },
    }),
    [reduce],
  );

  const selectedCount = topics.length;

  return (
    <section
      aria-label="Newsletter sign-up"
      className="relative w-full overflow-hidden bg-slate-50 px-4 py-20 text-slate-900 sm:px-6 sm:py-28 dark:bg-slate-950 dark:text-slate-100"
    >
      <style>{`
        @keyframes newsx-sweep {
          0% { transform: translateX(-120%) skewX(-12deg); }
          100% { transform: translateX(240%) skewX(-12deg); }
        }
        @keyframes newsx-float {
          0%, 100% { transform: translateY(0); }
          50% { transform: translateY(-5px); }
        }
        @keyframes newsx-pulse-ring {
          0% { transform: scale(0.85); opacity: 0.55; }
          70% { transform: scale(1.5); opacity: 0; }
          100% { opacity: 0; }
        }
        @keyframes newsx-draw {
          from { stroke-dashoffset: 30; }
          to { stroke-dashoffset: 0; }
        }
        .newsx-sweep { animation: newsx-sweep 3.4s ease-in-out infinite; }
        .newsx-float { animation: newsx-float 4.5s ease-in-out infinite; }
        .newsx-ring { animation: newsx-pulse-ring 2.4s ease-out infinite; }
        .newsx-check { stroke-dasharray: 30; animation: newsx-draw 0.6s ease-out forwards; }
        @media (prefers-reduced-motion: reduce) {
          .newsx-sweep, .newsx-float, .newsx-ring, .newsx-check {
            animation: none !important;
          }
          .newsx-check { stroke-dashoffset: 0; }
        }
      `}</style>

      {/* Atmosphere */}
      <div
        aria-hidden="true"
        className="pointer-events-none absolute inset-0 [background-image:radial-gradient(circle_at_18%_20%,rgba(99,102,241,0.10),transparent_45%),radial-gradient(circle_at_82%_78%,rgba(139,92,246,0.10),transparent_45%)]"
      />
      <div
        aria-hidden="true"
        className="pointer-events-none absolute inset-0 opacity-[0.04] [background-image:linear-gradient(to_right,currentColor_1px,transparent_1px),linear-gradient(to_bottom,currentColor_1px,transparent_1px)] [background-size:44px_44px]"
      />

      {/* Preview / trigger surface */}
      <div className="relative mx-auto flex max-w-3xl flex-col items-center text-center">
        <span className="inline-flex items-center gap-2 rounded-full border border-indigo-200 bg-white/70 px-3 py-1 text-xs font-semibold uppercase tracking-[0.16em] text-indigo-700 backdrop-blur dark:border-indigo-400/25 dark:bg-slate-900/60 dark:text-indigo-300">
          <span className="relative flex h-1.5 w-1.5">
            <span className="newsx-ring absolute inline-flex h-full w-full rounded-full bg-indigo-500" />
            <span className="relative inline-flex h-1.5 w-1.5 rounded-full bg-indigo-500" />
          </span>
          Issue 048 · Friday
        </span>

        <h2 className="mt-6 text-balance text-3xl font-semibold tracking-tight sm:text-4xl">
          The three things in product engineering worth your Friday.
        </h2>
        <p className="mt-4 max-w-xl text-pretty text-base leading-relaxed text-slate-600 dark:text-slate-400">
          Signal Weekly is one short email — no growth hacks, no listicles.
          Just the work our team actually paid attention to this week.
        </p>

        <button
          type="button"
          onClick={(e) => openModal(e.currentTarget)}
          className="group mt-8 inline-flex items-center gap-2 rounded-full bg-slate-900 px-6 py-3 text-sm font-semibold text-white shadow-lg shadow-slate-900/20 outline-none transition-transform hover:-translate-y-0.5 focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 dark:bg-white dark:text-slate-900 dark:shadow-black/40 dark:focus-visible:ring-offset-slate-950"
        >
          Open the sign-up card
          <svg
            viewBox="0 0 24 24"
            className="h-4 w-4 transition-transform group-hover:translate-x-0.5"
            fill="none"
            stroke="currentColor"
            strokeWidth={2}
            strokeLinecap="round"
            strokeLinejoin="round"
            aria-hidden="true"
          >
            <path d="M5 12h14M13 6l6 6-6 6" />
          </svg>
        </button>
      </div>

      <AnimatePresence>
        {open && (
          <motion.div
            key="newsx-backdrop"
            variants={backdropVariants}
            initial="hidden"
            animate="show"
            exit="hidden"
            transition={{ duration: reduce ? 0 : 0.22 }}
            onMouseDown={onBackdrop}
            className="fixed inset-0 z-50 flex items-end justify-center bg-slate-950/60 p-4 backdrop-blur-sm sm:items-center"
          >
            <motion.div
              ref={dialogRef}
              role="dialog"
              aria-modal="true"
              aria-labelledby={titleId}
              aria-describedby={descId}
              onKeyDown={onKeyDown}
              variants={cardVariants}
              initial="hidden"
              animate="show"
              exit="exit"
              transition={{ duration: dur, ease: [0.16, 1, 0.3, 1] }}
              className="relative w-full max-w-md overflow-hidden rounded-3xl border border-slate-200 bg-white shadow-2xl shadow-slate-900/25 dark:border-slate-700/70 dark:bg-slate-900 dark:shadow-black/60"
            >
              {/* Close */}
              <button
                type="button"
                onClick={closeModal}
                aria-label="Close newsletter sign-up"
                className="absolute right-3 top-3 z-20 inline-flex h-9 w-9 items-center justify-center rounded-full text-slate-500 outline-none transition-colors hover:bg-slate-100 hover:text-slate-900 focus-visible:ring-2 focus-visible:ring-indigo-500 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-white"
              >
                <svg
                  viewBox="0 0 24 24"
                  className="h-5 w-5"
                  fill="none"
                  stroke="currentColor"
                  strokeWidth={2}
                  strokeLinecap="round"
                  aria-hidden="true"
                >
                  <path d="M6 6l12 12M18 6L6 18" />
                </svg>
              </button>

              {/* Header band */}
              <div className="relative overflow-hidden bg-gradient-to-br from-indigo-600 via-violet-600 to-indigo-700 px-6 pb-8 pt-9">
                <div
                  aria-hidden="true"
                  className="pointer-events-none absolute inset-0 overflow-hidden"
                >
                  <span className="newsx-sweep absolute inset-y-0 left-0 w-1/3 bg-white/15 blur-md" />
                </div>
                <div className="relative flex items-center gap-4">
                  <div className="newsx-float flex h-14 w-14 shrink-0 items-center justify-center rounded-2xl bg-white/15 ring-1 ring-white/30 backdrop-blur">
                    <svg
                      viewBox="0 0 24 24"
                      className="h-7 w-7 text-white"
                      fill="none"
                      stroke="currentColor"
                      strokeWidth={1.7}
                      strokeLinecap="round"
                      strokeLinejoin="round"
                      aria-hidden="true"
                    >
                      <rect x="3" y="5" width="18" height="14" rx="2.5" />
                      <path d="m3.5 7 8.5 6 8.5-6" />
                    </svg>
                  </div>
                  <div>
                    <p className="text-xs font-semibold uppercase tracking-[0.18em] text-indigo-100/90">
                      Signal Weekly
                    </p>
                    <h3
                      id={titleId}
                      className="mt-1 text-xl font-semibold text-white"
                    >
                      Get the Friday dispatch
                    </h3>
                  </div>
                </div>
              </div>

              {/* Body */}
              <div className="px-6 pb-6 pt-5">
                <AnimatePresence mode="wait" initial={false}>
                  {status === "success" ? (
                    <motion.div
                      key="success"
                      initial={reduce ? false : { opacity: 0, y: 8 }}
                      animate={{ opacity: 1, y: 0 }}
                      transition={{ duration: reduce ? 0 : 0.28 }}
                      className="py-6 text-center"
                    >
                      <div className="mx-auto flex h-16 w-16 items-center justify-center rounded-full bg-emerald-100 dark:bg-emerald-500/15">
                        <svg
                          viewBox="0 0 24 24"
                          className="h-8 w-8 text-emerald-600 dark:text-emerald-400"
                          fill="none"
                          stroke="currentColor"
                          strokeWidth={2.4}
                          strokeLinecap="round"
                          strokeLinejoin="round"
                          aria-hidden="true"
                        >
                          <path className="newsx-check" d="M5 13l4 4L19 7" />
                        </svg>
                      </div>
                      <h4 className="mt-5 text-lg font-semibold text-slate-900 dark:text-white">
                        You're on the list.
                      </h4>
                      <p className="mx-auto mt-2 max-w-xs text-sm leading-relaxed text-slate-600 dark:text-slate-400">
                        We sent a confirmation to{" "}
                        <span className="font-medium text-slate-900 dark:text-slate-200">
                          {email.trim()}
                        </span>
                        . Click the link inside to lock in your first issue this
                        Friday.
                      </p>
                      <button
                        type="button"
                        onClick={closeModal}
                        className="mt-6 inline-flex items-center justify-center rounded-xl bg-slate-900 px-5 py-2.5 text-sm font-semibold text-white outline-none transition-colors hover:bg-slate-800 focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200 dark:focus-visible:ring-offset-slate-900"
                      >
                        Done
                      </button>
                    </motion.div>
                  ) : (
                    <motion.form
                      key="form"
                      onSubmit={onSubmit}
                      noValidate
                      initial={reduce ? false : { opacity: 0 }}
                      animate={{ opacity: 1 }}
                      transition={{ duration: reduce ? 0 : 0.2 }}
                    >
                      <p
                        id={descId}
                        className="text-sm leading-relaxed text-slate-600 dark:text-slate-400"
                      >
                        One email every Friday. Pick what you want to hear about —
                        skip the rest.
                      </p>

                      {/* Topics */}
                      <div className="mt-4" role="group" aria-labelledby={topicsLabelId}>
                        <div className="flex items-center justify-between">
                          <span
                            id={topicsLabelId}
                            className="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400"
                          >
                            Interests
                          </span>
                          <span className="text-xs text-slate-400 dark:text-slate-500">
                            {selectedCount} selected
                          </span>
                        </div>
                        <div className="mt-2 grid grid-cols-2 gap-2">
                          {TOPICS.map((topic) => {
                            const active = topics.includes(topic.id);
                            return (
                              <button
                                key={topic.id}
                                type="button"
                                aria-pressed={active}
                                onClick={() => toggleTopic(topic.id)}
                                className={[
                                  "group flex flex-col items-start gap-0.5 rounded-xl border px-3 py-2.5 text-left outline-none transition-all focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-900",
                                  active
                                    ? "border-indigo-500 bg-indigo-50 dark:border-indigo-400/60 dark:bg-indigo-500/10"
                                    : "border-slate-200 bg-white hover:border-slate-300 dark:border-slate-700 dark:bg-slate-800/50 dark:hover:border-slate-600",
                                ].join(" ")}
                              >
                                <span className="flex w-full items-center justify-between gap-1">
                                  <span
                                    className={[
                                      "text-sm font-semibold",
                                      active
                                        ? "text-indigo-700 dark:text-indigo-300"
                                        : "text-slate-800 dark:text-slate-200",
                                    ].join(" ")}
                                  >
                                    {topic.label}
                                  </span>
                                  <span
                                    aria-hidden="true"
                                    className={[
                                      "flex h-4 w-4 shrink-0 items-center justify-center rounded-full border transition-colors",
                                      active
                                        ? "border-indigo-500 bg-indigo-500 text-white dark:border-indigo-400 dark:bg-indigo-400"
                                        : "border-slate-300 text-transparent dark:border-slate-600",
                                    ].join(" ")}
                                  >
                                    <svg
                                      viewBox="0 0 24 24"
                                      className="h-2.5 w-2.5"
                                      fill="none"
                                      stroke="currentColor"
                                      strokeWidth={3.5}
                                      strokeLinecap="round"
                                      strokeLinejoin="round"
                                    >
                                      <path d="M5 13l4 4L19 7" />
                                    </svg>
                                  </span>
                                </span>
                                <span className="text-xs text-slate-500 dark:text-slate-400">
                                  {topic.hint}
                                </span>
                              </button>
                            );
                          })}
                        </div>
                      </div>

                      {/* Email */}
                      <div className="mt-4">
                        <label
                          htmlFor={`${uid}-email`}
                          className="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400"
                        >
                          Email address
                        </label>
                        <div className="relative mt-1.5">
                          <span
                            aria-hidden="true"
                            className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-slate-400 dark:text-slate-500"
                          >
                            <svg
                              viewBox="0 0 24 24"
                              className="h-[18px] w-[18px]"
                              fill="none"
                              stroke="currentColor"
                              strokeWidth={1.8}
                              strokeLinecap="round"
                              strokeLinejoin="round"
                            >
                              <rect x="3" y="5" width="18" height="14" rx="2.5" />
                              <path d="m3.5 7 8.5 6 8.5-6" />
                            </svg>
                          </span>
                          <input
                            ref={emailRef}
                            id={`${uid}-email`}
                            name="email"
                            type="email"
                            inputMode="email"
                            autoComplete="email"
                            placeholder="you@studio.com"
                            value={email}
                            onChange={onEmailChange}
                            onBlur={() => setTouched(true)}
                            aria-invalid={emailInvalid}
                            aria-describedby={emailInvalid ? errId : undefined}
                            className={[
                              "w-full rounded-xl border bg-white py-2.5 pl-10 pr-3 text-sm text-slate-900 outline-none transition-colors placeholder:text-slate-400 focus-visible:ring-2 focus-visible:ring-offset-1 focus-visible:ring-offset-white dark:bg-slate-800/60 dark:text-white dark:placeholder:text-slate-500 dark:focus-visible:ring-offset-slate-900",
                              emailInvalid
                                ? "border-rose-400 focus-visible:ring-rose-500 dark:border-rose-500/70"
                                : "border-slate-300 focus-visible:ring-indigo-500 dark:border-slate-700",
                            ].join(" ")}
                          />
                        </div>
                        <p
                          id={errId}
                          aria-live="polite"
                          className={[
                            "mt-1.5 text-xs text-rose-600 transition-opacity dark:text-rose-400",
                            emailInvalid ? "opacity-100" : "opacity-0",
                          ].join(" ")}
                        >
                          {emailInvalid
                            ? "Please enter a valid email address."
                            : " "}
                        </p>
                      </div>

                      {/* Submit */}
                      <button
                        type="submit"
                        disabled={status === "submitting"}
                        className="mt-1 flex w-full items-center justify-center gap-2 rounded-xl bg-gradient-to-r from-indigo-600 to-violet-600 px-5 py-3 text-sm font-semibold text-white shadow-lg shadow-indigo-600/25 outline-none transition-all hover:from-indigo-500 hover:to-violet-500 focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white disabled:cursor-not-allowed disabled:opacity-70 dark:focus-visible:ring-offset-slate-900"
                      >
                        {status === "submitting" ? (
                          <>
                            <svg
                              viewBox="0 0 24 24"
                              className="h-4 w-4 animate-spin"
                              fill="none"
                              aria-hidden="true"
                            >
                              <circle
                                cx="12"
                                cy="12"
                                r="9"
                                stroke="currentColor"
                                strokeWidth={3}
                                className="opacity-25"
                              />
                              <path
                                d="M21 12a9 9 0 0 0-9-9"
                                stroke="currentColor"
                                strokeWidth={3}
                                strokeLinecap="round"
                              />
                            </svg>
                            Signing you up…
                          </>
                        ) : (
                          <>
                            Subscribe free
                            <svg
                              viewBox="0 0 24 24"
                              className="h-4 w-4"
                              fill="none"
                              stroke="currentColor"
                              strokeWidth={2}
                              strokeLinecap="round"
                              strokeLinejoin="round"
                              aria-hidden="true"
                            >
                              <path d="M5 12h14M13 6l6 6-6 6" />
                            </svg>
                          </>
                        )}
                      </button>

                      {/* Social proof + trust */}
                      <div className="mt-4 flex items-center justify-center gap-3">
                        <div className="flex -space-x-2" aria-hidden="true">
                          {SUBSCRIBERS.map((s) => (
                            <span
                              key={s.initials}
                              className={[
                                "flex h-6 w-6 items-center justify-center rounded-full text-[10px] font-bold text-white ring-2 ring-white dark:ring-slate-900",
                                s.tone,
                              ].join(" ")}
                            >
                              {s.initials}
                            </span>
                          ))}
                        </div>
                        <p className="text-xs text-slate-500 dark:text-slate-400">
                          Joining{" "}
                          <span className="font-semibold text-slate-700 dark:text-slate-300">
                            12,400+
                          </span>{" "}
                          builders
                        </p>
                      </div>

                      <p className="mt-3 flex items-center justify-center gap-1.5 text-center text-xs text-slate-400 dark:text-slate-500">
                        <svg
                          viewBox="0 0 24 24"
                          className="h-3.5 w-3.5"
                          fill="none"
                          stroke="currentColor"
                          strokeWidth={1.8}
                          strokeLinecap="round"
                          strokeLinejoin="round"
                          aria-hidden="true"
                        >
                          <rect x="4" y="10" width="16" height="10" rx="2" />
                          <path d="M8 10V7a4 4 0 0 1 8 0v3" />
                        </svg>
                        No spam. Unsubscribe in one click.
                      </p>
                    </motion.form>
                  )}
                </AnimatePresence>
              </div>
            </motion.div>
          </motion.div>
        )}
      </AnimatePresence>
    </section>
  );
}

Dependencies

motion

Licence

Built by Web Innoventix. Free for personal and commercial use, no attribution required.

Built by Web Innoventix

Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.

Get a free quote

Similar components

Browse all →