# Sign-up Card

> A composable account creation form with consent, guidance, and verification.

- Category: Authentication
- Kind: component
- Page: https://uaiblocks.dev/components/sign-up-card
- Registry item: https://uaiblocks.dev/r/sign-up-card.json
- Source: https://github.com/victor-teles/uai/blob/main/src/registry/uai/components/sign-up-card.tsx

## Install

```bash
bunx shadcn@latest add https://uaiblocks.dev/r/sign-up-card.json
```

## Usage

```tsx
"use client";

import { Building2, Code2 } from "lucide-react";
import { type FormEvent, useEffect, useRef, useState } from "react";
import {
  SignUpCard,
  SignUpCardBody,
  SignUpCardCheckbox,
  SignUpCardConsent,
  SignUpCardDescription,
  SignUpCardDivider,
  SignUpCardError,
  SignUpCardField,
  SignUpCardFieldMessage,
  SignUpCardFields,
  SignUpCardFooter,
  SignUpCardHeader,
  SignUpCardInput,
  SignUpCardLabel,
  SignUpCardPasswordGuide,
  SignUpCardPasswordRequirement,
  SignUpCardProvider,
  SignUpCardProviders,
  type SignUpCardStatus,
  SignUpCardSubmit,
  SignUpCardTitle,
  type SignUpCardVariant,
  SignUpCardVerification,
} from "@/components/ui/uai/sign-up-card";

export function SignUpCardPreview({ variant = "card" }: { variant?: SignUpCardVariant }) {
  const [status, setStatus] = useState<SignUpCardStatus>("idle");
  const [password, setPassword] = useState("Preview123!");
  const timerRef = useRef<number | null>(null);
  const hasMinimumLength = password.length >= 8;
  const hasNumberOrSymbol = /[\d\W]/.test(password);

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

  function finishAccountCreation(success: boolean) {
    if (timerRef.current) window.clearTimeout(timerRef.current);
    setStatus("submitting");
    timerRef.current = window.setTimeout(() => setStatus(success ? "verification" : "error"), 800);
  }

  function handleSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    finishAccountCreation(hasMinimumLength && hasNumberOrSymbol);
  }

  return (
    <div
      className={
        variant === "split"
          ? "w-full max-w-[680px]"
          : variant === "compact"
            ? "w-full max-w-[320px]"
            : "w-full max-w-[400px]"
      }
    >
      <SignUpCard variant={variant} status={status} onSubmit={handleSubmit}>
        <SignUpCardHeader>
          <SignUpCardTitle>
            {status === "verification" ? "Check your inbox" : "Create your account"}
          </SignUpCardTitle>
          <SignUpCardDescription>
            {status === "verification"
              ? "We sent a verification link to hello@acme.co."
              : "Start with SSO or use your work email."}
          </SignUpCardDescription>
        </SignUpCardHeader>

        {status === "verification" ? (
          <SignUpCardVerification>
            <span>Open the link to finish creating your account.</span>
            <button
              type="button"
              className="inline-flex h-7 items-center rounded-full px-3 text-[12.5px] font-medium text-[var(--uai-muted)] transition-colors hover:bg-[var(--uai-surface-raised)] hover:text-[var(--uai-text)]"
              onClick={() => setStatus("idle")}
            >
              Use another email
            </button>
          </SignUpCardVerification>
        ) : (
          <SignUpCardBody>
            <SignUpCardProviders>
              <SignUpCardProvider onClick={() => finishAccountCreation(true)}>
                <Building2 strokeWidth={1.8} aria-hidden="true" />
                Continue with SSO
              </SignUpCardProvider>
              <SignUpCardProvider onClick={() => finishAccountCreation(true)}>
                <Code2 strokeWidth={1.8} aria-hidden="true" />
                Continue with GitHub
              </SignUpCardProvider>
            </SignUpCardProviders>
            <SignUpCardDivider />
            <SignUpCardFields>
              <SignUpCardField>
                <SignUpCardLabel>Name</SignUpCardLabel>
                <SignUpCardInput
                  name="name"
                  autoComplete="name"
                  defaultValue="Alex Morgan"
                  required
                />
              </SignUpCardField>
              <SignUpCardField>
                <SignUpCardLabel>Work email</SignUpCardLabel>
                <SignUpCardInput
                  name="email"
                  type="email"
                  autoComplete="email"
                  defaultValue="hello@acme.co"
                  required
                />
              </SignUpCardField>
              <SignUpCardField invalid={status === "error"}>
                <SignUpCardLabel>Password</SignUpCardLabel>
                <SignUpCardInput
                  name="password"
                  revealable
                  autoComplete="new-password"
                  aria-describedby="preview-password-requirements"
                  value={password}
                  onChange={(event) => {
                    setPassword(event.currentTarget.value);
                    if (status === "error") setStatus("idle");
                  }}
                  required
                />
                <SignUpCardPasswordGuide id="preview-password-requirements">
                  <SignUpCardPasswordRequirement met={hasMinimumLength}>
                    At least 8 characters
                  </SignUpCardPasswordRequirement>
                  <SignUpCardPasswordRequirement met={hasNumberOrSymbol}>
                    One number or symbol
                  </SignUpCardPasswordRequirement>
                </SignUpCardPasswordGuide>
                {status === "error" ? (
                  <SignUpCardFieldMessage>
                    Meet both password requirements and try again.
                  </SignUpCardFieldMessage>
                ) : null}
              </SignUpCardField>
              <SignUpCardConsent>
                <SignUpCardCheckbox name="terms" defaultChecked required />
                <span>
                  I agree to the <a href="#terms">Terms</a> and{" "}
                  <a href="#privacy">Privacy Policy</a>.
                </span>
              </SignUpCardConsent>
              <SignUpCardError>
                We could not create your account. Review the password and try again.
              </SignUpCardError>
              <SignUpCardSubmit>
                {status === "error" ? "Try again" : "Create account"}
              </SignUpCardSubmit>
            </SignUpCardFields>
          </SignUpCardBody>
        )}

        <SignUpCardFooter>
          Already have an account? <a href="#sign-in">Sign in</a>
        </SignUpCardFooter>
      </SignUpCard>
    </div>
  );
}
```

## Anatomy

- `SignUpCard`
  - `SignUpCardHeader`
    - `SignUpCardTitle`
    - `SignUpCardDescription`
    - `SignUpCardVerification`
    - `SignUpCardBody`
      - `SignUpCardProviders`
        - `SignUpCardProvider`
      - `SignUpCardDivider`
      - `SignUpCardFields`
        - `SignUpCardField`
          - `SignUpCardLabel`
          - `SignUpCardInput`
          - `SignUpCardPasswordGuide`
            - `SignUpCardPasswordRequirement`
            - `SignUpCardFieldMessage`
        - `SignUpCardConsent`
          - `SignUpCardCheckbox`
        - `SignUpCardError`
        - `SignUpCardSubmit`
  - `SignUpCardFooter`

## Accessibility

- The form is labelled by its composed heading and keeps native submit behavior.
- Every account field has a programmatic label and browser autocomplete purpose.
- Password requirements include visible met and not-met text in addition to icons.
- Terms consent uses a required checkbox (aria-required) and remains consumer-authored.
- Submitting disables duplicate provider, field, consent, and submit actions.
- Verification feedback uses a polite status while errors use assertive alerts.
- Card, split, and compact variants preserve the same form and keyboard contract.
