Web InnoventixFreeCode

Fullscreen Modal

Original · free

fullscreen modal

byWeb InnoventixReact + Tailwind
modalfullscreenmodals
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/modal-fullscreen.json
modal-fullscreen.tsx
"use client";

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

const CHANNELS = [
  { id: "stable", label: "Stable", hint: "Everyone on the default track" },
  { id: "beta", label: "Beta", hint: "Opt-in early adopters" },
  { id: "canary", label: "Canary", hint: "Internal & power users" },
] as const;

type ChannelId = (typeof CHANNELS)[number]["id"];

const HIGHLIGHT_LIMIT = 280;
const SUBSCRIBERS = 12480;

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

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

  const [open, setOpen] = useState(false);
  const [status, setStatus] = useState<"editing" | "published">("editing");

  const [version, setVersion] = useState("2.4.0");
  const [title, setTitle] = useState("Realtime collaboration, everywhere");
  const [highlights, setHighlights] = useState(
    "Cursors, comments, and presence now sync in under 40ms. Offline edits reconcile automatically when you reconnect.",
  );
  const [channel, setChannel] = useState<ChannelId>("beta");
  const [notify, setNotify] = useState(true);

  const dialogRef = useRef<HTMLDivElement | null>(null);
  const initialFocusRef = useRef<HTMLInputElement | null>(null);
  const triggerRef = useRef<HTMLButtonElement | null>(null);
  const previouslyFocused = useRef<HTMLElement | null>(null);
  const channelRefs = useRef<(HTMLButtonElement | null)[]>([]);

  const headingId = useId();
  const descId = useId();
  const versionId = useId();
  const titleId = useId();
  const highlightsId = useId();
  const counterId = useId();
  const channelLabelId = useId();
  const notifyLabelId = useId();
  const notifyDescId = useId();

  const overLimit = highlights.length > HIGHLIGHT_LIMIT;
  const remaining = HIGHLIGHT_LIMIT - highlights.length;
  const canPublish =
    version.trim().length > 0 && title.trim().length > 0 && !overLimit;

  const openModal = useCallback(() => {
    setStatus("editing");
    setOpen(true);
  }, []);

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

  // Focus trap, scroll lock, Escape + Tab handling, focus restore.
  useEffect(() => {
    if (!open) return;

    const dialog = dialogRef.current;
    previouslyFocused.current =
      (document.activeElement as HTMLElement | null) ?? triggerRef.current;

    const prevOverflow = document.body.style.overflow;
    document.body.style.overflow = "hidden";

    const focusStart = window.setTimeout(() => {
      if (initialFocusRef.current) initialFocusRef.current.focus();
      else dialog?.focus();
    }, 0);

    const onKeyDown = (event: KeyboardEvent) => {
      if (event.key === "Escape") {
        event.preventDefault();
        setOpen(false);
        return;
      }
      if (event.key !== "Tab" || !dialog) return;

      const nodes = Array.from(
        dialog.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR),
      ).filter((el) => el.getClientRects().length > 0);
      if (nodes.length === 0) {
        event.preventDefault();
        return;
      }

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

      if (event.shiftKey) {
        if (active === first || !dialog.contains(active)) {
          event.preventDefault();
          last.focus();
        }
      } else if (active === last || !dialog.contains(active)) {
        event.preventDefault();
        first.focus();
      }
    };

    document.addEventListener("keydown", onKeyDown, true);

    return () => {
      window.clearTimeout(focusStart);
      document.removeEventListener("keydown", onKeyDown, true);
      document.body.style.overflow = prevOverflow;
      const restore = previouslyFocused.current;
      if (restore && typeof restore.focus === "function") restore.focus();
    };
  }, [open]);

  const onChannelKeyDown = (
    event: ReactKeyboardEvent<HTMLButtonElement>,
    index: number,
  ) => {
    let next = index;
    if (event.key === "ArrowRight" || event.key === "ArrowDown") {
      next = (index + 1) % CHANNELS.length;
    } else if (event.key === "ArrowLeft" || event.key === "ArrowUp") {
      next = (index - 1 + CHANNELS.length) % CHANNELS.length;
    } else if (event.key === "Home") {
      next = 0;
    } else if (event.key === "End") {
      next = CHANNELS.length - 1;
    } else {
      return;
    }
    event.preventDefault();
    setChannel(CHANNELS[next].id);
    channelRefs.current[next]?.focus();
  };

  const dur = reduce ? 0 : 0.42;

  const backdropVariants: Variants = {
    hidden: { opacity: 0 },
    visible: { opacity: 1, transition: { duration: dur * 0.6 } },
    exit: { opacity: 0, transition: { duration: dur * 0.5 } },
  };

  const panelVariants: Variants = {
    hidden: { opacity: 0, scale: reduce ? 1 : 0.985, y: reduce ? 0 : 14 },
    visible: {
      opacity: 1,
      scale: 1,
      y: 0,
      transition: { duration: dur, ease: "easeOut" },
    },
    exit: {
      opacity: 0,
      scale: reduce ? 1 : 0.99,
      y: reduce ? 0 : 8,
      transition: { duration: dur * 0.6, ease: "easeIn" },
    },
  };

  const currentChannel = CHANNELS.find((c) => c.id === channel) ?? CHANNELS[0];

  return (
    <section className="relative w-full overflow-hidden bg-slate-50 px-5 py-20 text-slate-900 sm:px-8 md:py-28 dark:bg-slate-950 dark:text-slate-100">
      <style>{`
        @keyframes fsm_aurora {
          0%   { transform: translate3d(-8%, 0, 0) scale(1.05); opacity: .7; }
          50%  { transform: translate3d(8%, -6%, 0) scale(1.15); opacity: 1; }
          100% { transform: translate3d(-8%, 0, 0) scale(1.05); opacity: .7; }
        }
        @keyframes fsm_sheen {
          0%   { background-position: 0% 50%; }
          100% { background-position: 200% 50%; }
        }
        .fsm-aurora { animation: fsm_aurora 14s ease-in-out infinite; }
        .fsm-sheen {
          background-size: 200% 100%;
          animation: fsm_sheen 6s linear infinite;
        }
        @media (prefers-reduced-motion: reduce) {
          .fsm-aurora, .fsm-sheen { animation: none !important; }
        }
      `}</style>

      {/* Ambient backdrop texture */}
      <div
        aria-hidden="true"
        className="pointer-events-none absolute inset-0 -z-10 opacity-70"
      >
        <div className="fsm-aurora absolute -top-24 left-1/2 h-72 w-[42rem] -translate-x-1/2 rounded-full bg-gradient-to-tr from-indigo-300/40 via-violet-300/30 to-sky-200/40 blur-3xl dark:from-indigo-600/25 dark:via-violet-600/20 dark:to-sky-700/20" />
        <div
          className="absolute inset-0 opacity-[0.35] dark:opacity-[0.25]"
          style={{
            backgroundImage:
              "linear-gradient(to right, rgba(100,116,139,.12) 1px, transparent 1px), linear-gradient(to bottom, rgba(100,116,139,.12) 1px, transparent 1px)",
            backgroundSize: "44px 44px",
            maskImage:
              "radial-gradient(ellipse 70% 60% at 50% 40%, black, transparent)",
            WebkitMaskImage:
              "radial-gradient(ellipse 70% 60% at 50% 40%, black, transparent)",
          }}
        />
      </div>

      {/* ---- Trigger / context canvas ---- */}
      <div className="mx-auto max-w-2xl text-center">
        <span className="inline-flex items-center gap-2 rounded-full border border-slate-300/70 bg-white/70 px-3 py-1 text-xs font-medium tracking-wide text-slate-600 backdrop-blur dark:border-slate-700/70 dark:bg-slate-900/60 dark:text-slate-300">
          <span className="relative flex h-2 w-2">
            <span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-emerald-400 opacity-60" />
            <span className="relative inline-flex h-2 w-2 rounded-full bg-emerald-500" />
          </span>
          Release console
        </span>

        <h2 className="mt-6 text-balance text-3xl font-semibold tracking-tight sm:text-4xl">
          Ship version{" "}
          <span className="bg-gradient-to-r from-indigo-600 to-violet-500 bg-clip-text text-transparent dark:from-indigo-400 dark:to-violet-300">
            {version || "—"}
          </span>
        </h2>
        <p className="mx-auto mt-4 max-w-md text-pretty text-sm leading-relaxed text-slate-600 dark:text-slate-400">
          Draft the changelog, choose a rollout channel, and publish to the{" "}
          <span className="font-medium text-slate-800 dark:text-slate-200">
            {currentChannel.label.toLowerCase()}
          </span>{" "}
          track. The composer opens full-screen so nothing distracts the launch.
        </p>

        <button
          ref={triggerRef}
          type="button"
          onClick={openModal}
          aria-haspopup="dialog"
          aria-expanded={open}
          className="group mt-8 inline-flex items-center gap-2.5 rounded-xl bg-slate-900 px-5 py-3 text-sm font-semibold text-white shadow-lg shadow-slate-900/20 transition hover:-translate-y-0.5 hover:bg-slate-800 focus-visible:outline-none 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:hover:bg-slate-100 dark:focus-visible:ring-offset-slate-950"
        >
          <RocketIcon className="h-4 w-4 transition-transform duration-300 group-hover:-translate-y-0.5 group-hover:translate-x-0.5" />
          Open release composer
        </button>
        <p className="mt-3 text-xs text-slate-500 dark:text-slate-500">
          Press{" "}
          <kbd className="rounded border border-slate-300 bg-white px-1.5 py-0.5 font-mono text-[11px] text-slate-600 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-400">
            Esc
          </kbd>{" "}
          to dismiss ·{" "}
          <kbd className="rounded border border-slate-300 bg-white px-1.5 py-0.5 font-mono text-[11px] text-slate-600 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-400">
            Tab
          </kbd>{" "}
          stays inside
        </p>
      </div>

      {/* ---- Fullscreen modal ---- */}
      <AnimatePresence>
        {open && (
          <motion.div
            key="fsm-backdrop"
            variants={backdropVariants}
            initial="hidden"
            animate="visible"
            exit="exit"
            className="fixed inset-0 z-50 bg-slate-950/40 backdrop-blur-sm"
          >
            <motion.div
              key="fsm-panel"
              ref={dialogRef}
              role="dialog"
              aria-modal="true"
              aria-labelledby={headingId}
              aria-describedby={descId}
              tabIndex={-1}
              variants={panelVariants}
              initial="hidden"
              animate="visible"
              exit="exit"
              className="fixed inset-0 flex flex-col bg-white outline-none dark:bg-slate-950"
            >
              {/* Header */}
              <header className="relative shrink-0 border-b border-slate-200 bg-white/80 backdrop-blur dark:border-slate-800 dark:bg-slate-950/80">
                <div
                  aria-hidden="true"
                  className="fsm-sheen absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-indigo-500 to-transparent"
                />
                <div className="mx-auto flex w-full max-w-3xl items-center justify-between gap-4 px-5 py-4 sm:px-8">
                  <div className="flex min-w-0 items-center gap-3">
                    <span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-gradient-to-br from-indigo-500 to-violet-600 text-white shadow-sm">
                      <RocketIcon className="h-5 w-5" />
                    </span>
                    <div className="min-w-0">
                      <h2
                        id={headingId}
                        className="truncate text-base font-semibold tracking-tight text-slate-900 dark:text-slate-50"
                      >
                        New release
                      </h2>
                      <p
                        id={descId}
                        className="truncate text-xs text-slate-500 dark:text-slate-400"
                      >
                        Draft · autosaved just now
                      </p>
                    </div>
                  </div>
                  <button
                    type="button"
                    onClick={closeModal}
                    aria-label="Close release composer"
                    className="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-lg text-slate-500 transition hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-50"
                  >
                    <CloseIcon className="h-5 w-5" />
                  </button>
                </div>
              </header>

              {/* Body */}
              <div className="relative flex-1 overflow-y-auto">
                {status === "published" ? (
                  <div className="mx-auto flex min-h-full w-full max-w-3xl flex-col items-center justify-center px-5 py-16 text-center sm:px-8">
                    <span className="flex h-16 w-16 items-center justify-center rounded-2xl bg-emerald-100 text-emerald-600 dark:bg-emerald-500/15 dark:text-emerald-400">
                      <CheckIcon className="h-8 w-8" />
                    </span>
                    <h3 className="mt-6 text-2xl font-semibold tracking-tight text-slate-900 dark:text-slate-50">
                      {version} is live on {currentChannel.label}
                    </h3>
                    <p className="mt-3 max-w-sm text-sm leading-relaxed text-slate-600 dark:text-slate-400">
                      {notify
                        ? `Release notes are rolling out and ${SUBSCRIBERS.toLocaleString()} subscribers are being emailed now.`
                        : "Release notes are rolling out. No subscriber email was sent."}
                    </p>
                    <button
                      type="button"
                      onClick={closeModal}
                      className="mt-8 inline-flex items-center gap-2 rounded-xl bg-slate-900 px-5 py-2.5 text-sm font-semibold text-white transition hover:bg-slate-800 focus-visible:outline-none 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-100 dark:focus-visible:ring-offset-slate-950"
                    >
                      Back to console
                    </button>
                  </div>
                ) : (
                  <form
                    onSubmit={(e) => e.preventDefault()}
                    className="mx-auto w-full max-w-3xl space-y-10 px-5 py-8 sm:px-8 sm:py-10"
                  >
                    {/* Version + title */}
                    <div className="grid gap-5 sm:grid-cols-[9rem_1fr]">
                      <div>
                        <label
                          htmlFor={versionId}
                          className="mb-1.5 block text-sm font-medium text-slate-700 dark:text-slate-300"
                        >
                          Version
                        </label>
                        <div className="relative">
                          <span className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 font-mono text-sm text-slate-400 dark:text-slate-500">
                            v
                          </span>
                          <input
                            ref={initialFocusRef}
                            id={versionId}
                            value={version}
                            onChange={(e: ChangeEvent<HTMLInputElement>) =>
                              setVersion(e.target.value)
                            }
                            inputMode="decimal"
                            placeholder="2.4.0"
                            className="w-full rounded-lg border border-slate-300 bg-white py-2.5 pl-7 pr-3 font-mono text-sm text-slate-900 shadow-sm transition placeholder:text-slate-400 focus:border-indigo-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500/40 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:placeholder:text-slate-600 dark:focus:border-indigo-400"
                          />
                        </div>
                      </div>
                      <div>
                        <label
                          htmlFor={titleId}
                          className="mb-1.5 block text-sm font-medium text-slate-700 dark:text-slate-300"
                        >
                          Release title
                        </label>
                        <input
                          id={titleId}
                          value={title}
                          onChange={(e: ChangeEvent<HTMLInputElement>) =>
                            setTitle(e.target.value)
                          }
                          placeholder="A short, human summary"
                          className="w-full rounded-lg border border-slate-300 bg-white px-3 py-2.5 text-sm text-slate-900 shadow-sm transition placeholder:text-slate-400 focus:border-indigo-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500/40 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:placeholder:text-slate-600 dark:focus:border-indigo-400"
                        />
                      </div>
                    </div>

                    {/* Highlights */}
                    <div>
                      <div className="mb-1.5 flex items-baseline justify-between gap-3">
                        <label
                          htmlFor={highlightsId}
                          className="text-sm font-medium text-slate-700 dark:text-slate-300"
                        >
                          Highlights
                        </label>
                        <span
                          id={counterId}
                          aria-live="polite"
                          className={`font-mono text-xs tabular-nums ${
                            overLimit
                              ? "text-rose-600 dark:text-rose-400"
                              : "text-slate-400 dark:text-slate-500"
                          }`}
                        >
                          {remaining}
                        </span>
                      </div>
                      <textarea
                        id={highlightsId}
                        value={highlights}
                        onChange={(e: ChangeEvent<HTMLTextAreaElement>) =>
                          setHighlights(e.target.value)
                        }
                        rows={4}
                        aria-describedby={counterId}
                        aria-invalid={overLimit}
                        placeholder="What changed, and why it matters."
                        className={`w-full resize-y rounded-lg border bg-white px-3 py-2.5 text-sm leading-relaxed text-slate-900 shadow-sm transition placeholder:text-slate-400 focus-visible:outline-none focus-visible:ring-2 dark:bg-slate-900 dark:text-slate-100 dark:placeholder:text-slate-600 ${
                          overLimit
                            ? "border-rose-400 focus:border-rose-500 focus-visible:ring-rose-500/40 dark:border-rose-500/60"
                            : "border-slate-300 focus:border-indigo-500 focus-visible:ring-indigo-500/40 dark:border-slate-700 dark:focus:border-indigo-400"
                        }`}
                      />
                      {overLimit && (
                        <p className="mt-1.5 text-xs text-rose-600 dark:text-rose-400">
                          Trim {Math.abs(remaining)} character
                          {Math.abs(remaining) === 1 ? "" : "s"} to stay under
                          the {HIGHLIGHT_LIMIT}-character summary limit.
                        </p>
                      )}
                    </div>

                    {/* Channel — roving-tabindex radiogroup */}
                    <div>
                      <span
                        id={channelLabelId}
                        className="mb-2 block text-sm font-medium text-slate-700 dark:text-slate-300"
                      >
                        Rollout channel
                      </span>
                      <div
                        role="radiogroup"
                        aria-labelledby={channelLabelId}
                        className="grid gap-3 sm:grid-cols-3"
                      >
                        {CHANNELS.map((c, i) => {
                          const selected = c.id === channel;
                          return (
                            <button
                              key={c.id}
                              ref={(el) => {
                                channelRefs.current[i] = el;
                              }}
                              type="button"
                              role="radio"
                              aria-checked={selected}
                              tabIndex={selected ? 0 : -1}
                              onClick={() => setChannel(c.id)}
                              onKeyDown={(e) => onChannelKeyDown(e, i)}
                              className={`relative rounded-xl border p-4 text-left transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-950 ${
                                selected
                                  ? "border-indigo-500 bg-indigo-50/70 shadow-sm dark:border-indigo-400 dark:bg-indigo-500/10"
                                  : "border-slate-200 bg-white hover:border-slate-300 hover:bg-slate-50 dark:border-slate-800 dark:bg-slate-900 dark:hover:border-slate-700"
                              }`}
                            >
                              <span className="flex items-center justify-between">
                                <span
                                  className={`text-sm font-semibold ${
                                    selected
                                      ? "text-indigo-700 dark:text-indigo-300"
                                      : "text-slate-800 dark:text-slate-200"
                                  }`}
                                >
                                  {c.label}
                                </span>
                                <span
                                  aria-hidden="true"
                                  className={`flex h-4 w-4 items-center justify-center rounded-full border ${
                                    selected
                                      ? "border-indigo-500 bg-indigo-500 dark:border-indigo-400 dark:bg-indigo-400"
                                      : "border-slate-300 dark:border-slate-600"
                                  }`}
                                >
                                  {selected && (
                                    <span className="h-1.5 w-1.5 rounded-full bg-white" />
                                  )}
                                </span>
                              </span>
                              <span className="mt-1 block text-xs text-slate-500 dark:text-slate-400">
                                {c.hint}
                              </span>
                            </button>
                          );
                        })}
                      </div>
                    </div>

                    {/* Notify — ARIA switch */}
                    <div className="flex items-center justify-between gap-4 rounded-xl border border-slate-200 bg-slate-50/70 p-4 dark:border-slate-800 dark:bg-slate-900/60">
                      <div className="min-w-0">
                        <p
                          id={notifyLabelId}
                          className="text-sm font-medium text-slate-800 dark:text-slate-200"
                        >
                          Email subscribers
                        </p>
                        <p
                          id={notifyDescId}
                          className="mt-0.5 text-xs text-slate-500 dark:text-slate-400"
                        >
                          Send these notes to {SUBSCRIBERS.toLocaleString()}{" "}
                          people watching this project.
                        </p>
                      </div>
                      <button
                        type="button"
                        role="switch"
                        aria-checked={notify}
                        aria-labelledby={notifyLabelId}
                        aria-describedby={notifyDescId}
                        onClick={() => setNotify((v) => !v)}
                        className={`relative inline-flex h-6 w-11 shrink-0 items-center rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-50 dark:focus-visible:ring-offset-slate-950 ${
                          notify
                            ? "bg-emerald-500 dark:bg-emerald-500"
                            : "bg-slate-300 dark:bg-slate-700"
                        }`}
                      >
                        <span
                          className={`inline-block h-5 w-5 transform rounded-full bg-white shadow transition-transform ${
                            notify ? "translate-x-5" : "translate-x-0.5"
                          }`}
                        />
                      </button>
                    </div>
                  </form>
                )}
              </div>

              {/* Footer */}
              {status === "editing" && (
                <footer className="shrink-0 border-t border-slate-200 bg-white/85 backdrop-blur dark:border-slate-800 dark:bg-slate-950/85">
                  <div className="mx-auto flex w-full max-w-3xl flex-col-reverse items-stretch gap-3 px-5 py-4 sm:flex-row sm:items-center sm:justify-between sm:px-8">
                    <p className="text-xs text-slate-500 dark:text-slate-400">
                      Publishing to{" "}
                      <span className="font-medium text-slate-700 dark:text-slate-300">
                        {currentChannel.label}
                      </span>
                      {notify ? " · notifying subscribers" : " · no email"}
                    </p>
                    <div className="flex items-center gap-3">
                      <button
                        type="button"
                        onClick={closeModal}
                        className="flex-1 rounded-lg border border-slate-300 bg-white px-4 py-2.5 text-sm font-medium text-slate-700 transition hover:bg-slate-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-400 sm:flex-none dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200 dark:hover:bg-slate-800"
                      >
                        Discard
                      </button>
                      <button
                        type="button"
                        onClick={() => canPublish && setStatus("published")}
                        disabled={!canPublish}
                        className="flex-1 inline-flex items-center justify-center gap-2 rounded-lg bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm transition hover:bg-indigo-500 focus-visible:outline-none 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-50 disabled:hover:bg-indigo-600 sm:flex-none dark:focus-visible:ring-offset-slate-950"
                      >
                        <RocketIcon className="h-4 w-4" />
                        Publish release
                      </button>
                    </div>
                  </div>
                </footer>
              )}
            </motion.div>
          </motion.div>
        )}
      </AnimatePresence>
    </section>
  );
}

/* ---------------- Inline icons ---------------- */

function RocketIcon({ className }: { className?: string }) {
  return (
    <svg
      className={className}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={1.75}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
    >
      <path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09Z" />
      <path d="M12 15l-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2Z" />
      <path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0" />
      <path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5" />
    </svg>
  );
}

function CloseIcon({ className }: { className?: string }) {
  return (
    <svg
      className={className}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={1.75}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
    >
      <path d="M18 6 6 18" />
      <path d="m6 6 12 12" />
    </svg>
  );
}

function CheckIcon({ className }: { className?: string }) {
  return (
    <svg
      className={className}
      viewBox="0 0 24 24"
      fill="none"
      stroke="currentColor"
      strokeWidth={2}
      strokeLinecap="round"
      strokeLinejoin="round"
      aria-hidden="true"
    >
      <path d="M20 6 9 17l-5-5" />
    </svg>
  );
}

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 →