# Auth Kit: complete sign-in flow

> Seven auth pages in three switchable layouts (centered, split with art, frosted glass): sign in, sign up with a live password checklist, six-digit verification, forgot and reset password, team invite and a three-step onboarding.

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

- Page, with a live preview: https://shadcn.exchange/r/ballmac/template-auth-kit
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/ballmac/template-auth-kit-light-7103d7f948b87b9f.png
- Registry item: https://ui.ballmac.com/r/template-auth-kit.json
- Categories: templates
- Listed: 2026-10-14, source last changed 2026-10-14

## Install

```bash
npx shadcn@latest add https://ui.ballmac.com/r/template-auth-kit.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/aurora-background.json`, `https://ui.ballmac.com/r/input-otp.json`, `https://ui.ballmac.com/r/segmented-control.json`

## Notes from the registry

Routes are at /auth/sign-in, /auth/sign-up, /auth/verify, /auth/forgot, /auth/reset, /auth/invite and /auth/onboarding. Each form simulates its request with a short timer: replace the useFake() calls in auth-kit-forms.tsx with your API.

## Files

### `registry/ballmac/components/templates/auth-kit/auth-kit-fonts.ts`

```ts
// Ballmac UI: Auth Kit template fonts. https://ui.ballmac.com/templates/template-auth-kit
import { Outfit } from "next/font/google"

export const authSans = Outfit({ variable: "--auth-sans", subsets: ["latin"], display: "swap", preload: false })
```

### `registry/ballmac/components/templates/auth-kit/auth-kit-theme.tsx`

```tsx
// Ballmac UI: Auth Kit template frame. https://ui.ballmac.com/templates/template-auth-kit
"use client"

import * as React from "react"
import { Eye, EyeOff, Loader2 } from "lucide-react"

import { AuroraBackground } from "@/components/ballmac/aurora-background"
import { authSans } from "@/components/ballmac/templates/auth-kit/auth-kit-fonts"
import { cn } from "@/lib/utils"

type AuthLayout = "centered" | "split" | "glass"
type AuthPage = "sign-in" | "sign-up" | "verify" | "forgot" | "reset" | "invite" | "onboarding"
type AuthHrefs = Record<AuthPage, string>

const defaultHrefs: AuthHrefs = {
  "sign-in": "/auth/sign-in",
  "sign-up": "/auth/sign-up",
  verify: "/auth/verify",
  forgot: "/auth/forgot",
  reset: "/auth/reset",
  invite: "/auth/invite",
  onboarding: "/auth/onboarding",
}

/** Auth Kit's palette: soft ivory and ink with one indigo. Friendly, clear and calm. */
const authCss = `
.auth-theme,body:not(:has([data-gallery])):has(.auth-theme){--background:oklch(0.982 0.006 90);--foreground:oklch(0.21 0.03 275);--card:oklch(1 0 0);--card-foreground:oklch(0.21 0.03 275);--popover:oklch(1 0 0);--popover-foreground:oklch(0.21 0.03 275);--primary:oklch(0.46 0.2 275);--primary-foreground:oklch(0.99 0.004 90);--secondary:oklch(0.958 0.01 90);--secondary-foreground:oklch(0.21 0.03 275);--muted:oklch(0.958 0.01 90);--muted-foreground:oklch(0.47 0.03 275);--accent:oklch(0.94 0.014 280);--accent-foreground:oklch(0.21 0.03 275);--border:oklch(0.21 0.03 275 / 12%);--input:oklch(0.21 0.03 275 / 20%);--ring:oklch(0.55 0.2 275);--surface:oklch(0.968 0.01 90);--destructive:oklch(0.53 0.22 25);--chart-1:oklch(0.5 0.2 275);--chart-2:oklch(0.48 0.12 165);--chart-3:oklch(0.62 0.14 75);--chart-4:oklch(0.52 0.19 340);--chart-5:oklch(0.52 0.12 235);--radius:0.875rem}
.dark .auth-theme,.dark body:not(:has([data-gallery])):has(.auth-theme){--background:oklch(0.17 0.022 275);--foreground:oklch(0.95 0.008 90);--card:oklch(0.21 0.026 275);--card-foreground:oklch(0.95 0.008 90);--popover:oklch(0.23 0.028 275);--popover-foreground:oklch(0.95 0.008 90);--primary:oklch(0.76 0.14 275);--primary-foreground:oklch(0.19 0.03 275);--secondary:oklch(0.26 0.028 275);--secondary-foreground:oklch(0.95 0.008 90);--muted:oklch(0.25 0.028 275);--muted-foreground:oklch(0.73 0.025 275);--accent:oklch(0.29 0.034 275);--accent-foreground:oklch(0.95 0.008 90);--border:oklch(1 0 0 / 11%);--input:oklch(1 0 0 / 16%);--ring:oklch(0.74 0.14 275);--surface:oklch(0.19 0.024 275);--destructive:oklch(0.7 0.19 25);--chart-1:oklch(0.76 0.14 275);--chart-2:oklch(0.78 0.13 165);--chart-3:oklch(0.83 0.13 85);--chart-4:oklch(0.76 0.15 340);--chart-5:oklch(0.76 0.11 235)}
body:not(:has([data-gallery])):has(.auth-theme){font-family:var(--auth-sans),ui-sans-serif,system-ui,sans-serif}
`

function AuthMark({ className }: { className?: string }) {
  return (
    <svg viewBox="0 0 28 28" aria-hidden="true" className={cn("size-8", className)} fill="none">
      <rect width="28" height="28" rx="9" fill="var(--primary)" />
      <circle cx="14" cy="12" r="3.6" stroke="var(--primary-foreground)" strokeWidth="2" />
      <path d="M14 15.6V21M14 18.5h2.6" stroke="var(--primary-foreground)" strokeWidth="2" strokeLinecap="round" />
    </svg>
  )
}

type AuthFrameProps = React.ComponentProps<"div"> & {
  /** How the page is composed. */
  layout?: AuthLayout
  /** Brand name beside the mark. */
  brand?: string
  /** Where the brand links to. */
  homeHref?: string
}

const quote = { text: "We replaced three login screens with one in an afternoon, and support tickets about passwords dropped by half.", name: "Sasha Petrova", role: "Head of Product, Fernhill" }

/** The page around an auth form: centred card, split with art, or frosted glass over a soft aurora. */
function AuthFrame({ layout = "centered", brand = "Keystone", homeHref = "#", className, style, children, ...props }: AuthFrameProps) {
  React.useEffect(() => {
    const cls = authSans.variable
    if (!cls) return
    document.body.classList.add(cls)
    return () => document.body.classList.remove(cls)
  }, [])
  const logo = (
    <a href={homeHref} className="focus-visible:ring-ring/50 inline-flex items-center gap-2.5 rounded-lg outline-none focus-visible:ring-[3px]">
      <AuthMark /> <span className="text-xl font-semibold tracking-tight">{brand}</span>
    </a>
  )
  return (
    <div
      data-slot="auth-kit"
      data-layout={layout}
      className={cn("auth-theme bg-background text-foreground relative isolate min-h-dvh overflow-x-clip", authSans.variable, className)}
      style={{ fontFamily: "var(--auth-sans), ui-sans-serif, system-ui, sans-serif", ...style }}
      {...props}
    >
      <style>{authCss}</style>
      {layout === "centered" && (
        <main className="flex min-h-dvh flex-col items-center justify-center px-4 py-12">
          <div aria-hidden="true" className="absolute inset-0 -z-10 [background-image:radial-gradient(var(--border)_1px,transparent_1px)] [background-size:22px_22px] [mask-image:radial-gradient(ellipse_at_center,black,transparent_70%)]" />
          <div className="mb-8">{logo}</div>
          <div className="bg-card w-full max-w-md rounded-3xl border p-7 shadow-[0_24px_60px_-30px_oklch(0.4_0.1_275/0.35)] sm:p-9">{children}</div>
          <p className="text-muted-foreground mt-8 text-xs">Protected by two-factor authentication · <a href="#" className="underline underline-offset-4">Privacy</a></p>
        </main>
      )}
      {layout === "split" && (
        <main className="grid min-h-dvh lg:grid-cols-[minmax(0,1fr)_minmax(0,1.05fr)]">
          <div className="flex flex-col px-6 py-8 sm:px-12 lg:px-16">
            {logo}
            <div className="mx-auto flex w-full max-w-sm flex-1 flex-col justify-center py-12">{children}</div>
            <p className="text-muted-foreground text-xs">© 2026 {brand}. All rights reserved.</p>
          </div>
          <aside aria-hidden="true" className="bg-primary text-primary-foreground relative hidden overflow-hidden lg:block">
            <div className="absolute inset-0 [background-image:radial-gradient(circle_at_20%_20%,var(--chart-4)_0,transparent_45%),radial-gradient(circle_at_85%_70%,var(--chart-5)_0,transparent_50%)] opacity-45" />
            <div className="absolute inset-0 [background-image:linear-gradient(oklch(1_0_0/0.07)_1px,transparent_1px),linear-gradient(90deg,oklch(1_0_0/0.07)_1px,transparent_1px)] [background-size:48px_48px]" />
            <figure className="relative flex h-full flex-col justify-end p-14">
              <blockquote className="max-w-md text-3xl leading-snug font-medium tracking-[-0.02em] text-balance">“{quote.text}”</blockquote>
              <figcaption className="mt-6 text-sm opacity-85"><span className="font-semibold">{quote.name}</span> · {quote.role}</figcaption>
            </figure>
          </aside>
        </main>
      )}
      {layout === "glass" && (
        <main className="flex min-h-dvh flex-col items-center justify-center px-4 py-12">
          <AuroraBackground aria-hidden="true" className="absolute inset-0 -z-10" colors={["var(--chart-1)", "var(--chart-4)", "var(--chart-5)"]} intensity={0.6} radialMask={false} />
          <div className="mb-8">{logo}</div>
          <div className="bg-card/55 w-full max-w-md rounded-3xl border p-7 shadow-[0_30px_80px_-30px_oklch(0.3_0.1_275/0.5)] backdrop-blur-2xl backdrop-saturate-150 sm:p-9">{children}</div>
        </main>
      )}
    </div>
  )
}

const inputClass = "bg-background focus-visible:ring-ring/50 placeholder:text-muted-foreground h-11 w-full rounded-xl border px-3.5 text-[15px] outline-none transition-shadow focus-visible:ring-[3px] aria-[invalid=true]:border-destructive"

/** A labelled text field with an inline error. */
function AuthField({ label, error, hint, id, className, ...props }: Omit<React.ComponentProps<"input">, "id"> & { label: string; error?: string; hint?: string; id: string }) {
  return (
    <div className={className}>
      <label htmlFor={id} className="text-sm font-medium">{label}</label>
      <input id={id} aria-invalid={!!error} aria-describedby={error ? `${id}-err` : hint ? `${id}-hint` : undefined} className={cn(inputClass, "mt-1.5")} {...props} />
      {error ? <p id={`${id}-err`} className="text-destructive mt-1.5 text-sm">{error}</p> : hint ? <p id={`${id}-hint`} className="text-muted-foreground mt-1.5 text-sm">{hint}</p> : null}
    </div>
  )
}

/** A password field with a show and hide button. */
function AuthPassword({ label = "Password", error, id, ...props }: Omit<React.ComponentProps<"input">, "id" | "type"> & { label?: string; error?: string; id: string }) {
  const [show, setShow] = React.useState(false)
  return (
    <div>
      <label htmlFor={id} className="text-sm font-medium">{label}</label>
      <div className="relative mt-1.5">
        <input id={id} type={show ? "text" : "password"} aria-invalid={!!error} aria-describedby={error ? `${id}-err` : undefined} className={cn(inputClass, "pe-11")} {...props} />
        <button type="button" onClick={() => setShow((v) => !v)} aria-label={show ? "Hide password" : "Show password"} aria-pressed={show} className="text-muted-foreground hover:text-foreground focus-visible:ring-ring/50 absolute top-1/2 end-1.5 inline-flex size-8 -translate-y-1/2 items-center justify-center rounded-lg outline-none focus-visible:ring-[3px]">
          {show ? <EyeOff className="size-4" aria-hidden="true" /> : <Eye className="size-4" aria-hidden="true" />}
        </button>
      </div>
      {error && <p id={`${id}-err`} className="text-destructive mt-1.5 text-sm">{error}</p>}
    </div>
  )
}

/** The main button of a form. */
function AuthButton({ busy, className, children, ...props }: React.ComponentProps<"button"> & { busy?: boolean }) {
  return (
    <button disabled={busy || props.disabled} className={cn("bg-primary text-primary-foreground focus-visible:ring-ring/50 inline-flex h-11 w-full items-center justify-center gap-2 rounded-xl text-[15px] font-semibold outline-none transition-opacity hover:opacity-90 focus-visible:ring-[3px] disabled:opacity-60", className)} {...props}>
      {busy && <Loader2 className="size-4 motion-safe:animate-spin" aria-hidden="true" />}
      {children}
    </button>
  )
}

/** Password rules, scored live. */
const rules = [
  { id: "len", label: "At least 10 characters", test: (p: string) => p.length >= 10 },
  { id: "case", label: "An upper and a lower case letter", test: (p: string) => /[a-z]/.test(p) && /[A-Z]/.test(p) },
  { id: "num", label: "A number", test: (p: string) => /\d/.test(p) },
  { id: "sym", label: "A symbol", test: (p: string) => /[^A-Za-z0-9]/.test(p) },
]

/** A strength bar and checklist for a new password. */
function PasswordStrength({ value, id }: { value: string; id: string }) {
  const passed = rules.filter((r) => r.test(value)).length
  const label = value.length === 0 ? "Enter a password" : passed <= 1 ? "Weak" : passed <= 3 ? "Fair" : "Strong"
  const tone = passed <= 1 ? "bg-destructive" : passed <= 3 ? "bg-chart-3" : "bg-chart-2"
  return (
    <div id={id} className="mt-3">
      <div className="flex gap-1.5" aria-hidden="true">
        {rules.map((r, i) => <span key={r.id} className={cn("h-1.5 flex-1 rounded-full transition-colors duration-300 motion-reduce:transition-none", i < passed && value ? tone : "bg-muted")} />)}
      </div>
      <p className="mt-2 text-sm font-medium" role="status">Strength: {label}</p>
      <ul className="text-muted-foreground mt-2 grid gap-1 text-sm">
        {rules.map((r) => {
          const ok = r.test(value)
          return <li key={r.id} className={cn("flex items-center gap-2", ok && "text-foreground")}><span className={cn("flex size-4 items-center justify-center rounded-full border text-[10px]", ok ? "bg-chart-2 text-background border-transparent" : "")} aria-hidden="true">{ok ? "✓" : ""}</span>{r.label}<span className="sr-only">{ok ? " (met)" : " (not met)"}</span></li>
        })}
      </ul>
    </div>
  )
}

export { AuthButton, AuthField, AuthFrame, AuthMark, AuthPassword, PasswordStrength, defaultHrefs as authDefaultHrefs, rules as passwordRules, type AuthFrameProps, type AuthHrefs, type AuthLayout, type AuthPage }
```

### `registry/ballmac/components/templates/auth-kit/auth-kit-forms.tsx`

```tsx
// Ballmac UI: Auth Kit forms. https://ui.ballmac.com/templates/template-auth-kit
"use client"

import * as React from "react"
import { ArrowLeft, ArrowRight, Check, MailCheck, ShieldCheck } from "lucide-react"

import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from "@/components/ballmac/input-otp"
import { AuthButton, AuthField, AuthPassword, PasswordStrength, authDefaultHrefs, passwordRules, type AuthHrefs } from "@/components/ballmac/templates/auth-kit/auth-kit-theme"
import { cn } from "@/lib/utils"

type FormProps = { hrefs?: Partial<AuthHrefs> }
const h = (hrefs: FormProps["hrefs"]) => ({ ...authDefaultHrefs, ...hrefs })
const link = "text-primary focus-visible:ring-ring/50 rounded font-medium underline-offset-4 outline-none hover:underline focus-visible:ring-[3px]"
const emailOk = (v: string) => /^\S+@\S+\.\S+$/.test(v.trim())

/** A short simulated request so busy and success states are visible. Swap for your API call. */
function useFake() {
  const timer = React.useRef<ReturnType<typeof setTimeout> | undefined>(undefined)
  React.useEffect(() => () => clearTimeout(timer.current), [])
  return (done: () => void, ms = 700) => {
    clearTimeout(timer.current)
    timer.current = setTimeout(done, ms)
  }
}

function Heading({ title, children }: { title: string; children?: React.ReactNode }) {
  return (
    <header className="mb-7">
      <h1 className="text-[1.75rem] leading-tight font-semibold tracking-[-0.03em] text-balance">{title}</h1>
      {children && <p className="text-muted-foreground mt-2 text-pretty">{children}</p>}
    </header>
  )
}

function Success({ title, children, action }: { title: string; children: React.ReactNode; action?: React.ReactNode }) {
  return (
    <div role="status">
      <span className="bg-chart-2/15 text-chart-2 flex size-12 items-center justify-center rounded-2xl"><Check className="size-6" aria-hidden="true" /></span>
      <h1 className="mt-5 text-[1.75rem] leading-tight font-semibold tracking-[-0.03em]">{title}</h1>
      <p className="text-muted-foreground mt-2 text-pretty">{children}</p>
      {action && <div className="mt-6">{action}</div>}
    </div>
  )
}

/** Sign in with email and password, or a provider. Validates before it submits. */
function SignInForm({ hrefs }: FormProps) {
  const links = h(hrefs)
  const fake = useFake()
  const [errors, setErrors] = React.useState<{ email?: string; password?: string }>({})
  const [busy, setBusy] = React.useState(false)
  const [done, setDone] = React.useState(false)
  function submit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault()
    const f = new FormData(e.currentTarget)
    const email = String(f.get("email") ?? "")
    const password = String(f.get("password") ?? "")
    const next: typeof errors = {}
    if (!emailOk(email)) next.email = "Enter an email like you@company.com."
    if (!password) next.password = "Enter your password."
    setErrors(next)
    if (next.email || next.password) {
      ;(e.currentTarget.elements.namedItem(next.email ? "email" : "password") as HTMLElement | null)?.focus()
      return
    }
    setBusy(true)
    fake(() => { setBusy(false); setDone(true) })
  }
  if (done) return <Success title="You’re in" action={<a className={link} href="#">Continue to your workspace →</a>}>Welcome back. We’ll take you to where you left off.</Success>
  return (
    <>
      <Heading title="Welcome back">Sign in to continue to your workspace.</Heading>
      <div className="grid grid-cols-2 gap-3">
        {["Google", "GitHub"].map((p) => <button key={p} type="button" className="hover:bg-accent focus-visible:ring-ring/50 h-11 rounded-xl border text-sm font-medium outline-none transition-colors focus-visible:ring-[3px]">{p}</button>)}
      </div>
      <div className="text-muted-foreground my-6 flex items-center gap-3 text-xs"><span className="bg-border h-px flex-1" />or with email<span className="bg-border h-px flex-1" /></div>
      <form onSubmit={submit} noValidate className="space-y-4">
        <AuthField id="si-email" name="email" type="email" label="Email" autoComplete="email" error={errors.email} />
        <div>
          <AuthPassword id="si-pass" name="password" autoComplete="current-password" error={errors.password} />
          <div className="mt-2.5 flex items-center justify-between text-sm">
            <label className="flex cursor-pointer items-center gap-2"><input type="checkbox" name="remember" className="accent-primary size-4" />Keep me signed in</label>
            <a href={links.forgot} className={link}>Forgot password?</a>
          </div>
        </div>
        <AuthButton type="submit" busy={busy}>{busy ? "Signing in…" : "Sign in"}</AuthButton>
      </form>
      <p className="text-muted-foreground mt-6 text-center text-sm">New here? <a href={links["sign-up"]} className={link}>Create an account</a></p>
    </>
  )
}

/** Create an account with a live password checklist and a terms checkbox. */
function SignUpForm({ hrefs }: FormProps) {
  const links = h(hrefs)
  const fake = useFake()
  const [pw, setPw] = React.useState("")
  const [errors, setErrors] = React.useState<{ name?: string; email?: string; password?: string; terms?: string }>({})
  const [busy, setBusy] = React.useState(false)
  const [email, setEmail] = React.useState<string | null>(null)
  function submit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault()
    const f = new FormData(e.currentTarget)
    const name = String(f.get("name") ?? "").trim()
    const mail = String(f.get("email") ?? "").trim()
    const next: typeof errors = {}
    if (!name) next.name = "Tell us your name."
    if (!emailOk(mail)) next.email = "Enter an email like you@company.com."
    if (!passwordRules.every((r) => r.test(pw))) next.password = "Meet every rule below to continue."
    if (!f.get("terms")) next.terms = "Agree to the terms to continue."
    setErrors(next)
    const first = (["name", "email", "password", "terms"] as const).find((k) => next[k])
    if (first) {
      ;(e.currentTarget.elements.namedItem(first) as HTMLElement | null)?.focus()
      return
    }
    setBusy(true)
    fake(() => { setBusy(false); setEmail(mail) })
  }
  if (email) return <Success title="Check your email" action={<a className={link} href={links.verify}>Enter the 6-digit code →</a>}>We sent a code to <span className="text-foreground font-medium">{email}</span>. It expires in 10 minutes.</Success>
  return (
    <>
      <Heading title="Create your account">Free for 14 days. No card needed.</Heading>
      <form onSubmit={submit} noValidate className="space-y-4">
        <AuthField id="su-name" name="name" label="Full name" autoComplete="name" error={errors.name} />
        <AuthField id="su-email" name="email" type="email" label="Work email" autoComplete="email" error={errors.email} />
        <div>
          <AuthPassword id="su-pass" name="password" label="Password" autoComplete="new-password" value={pw} onChange={(e) => setPw(e.target.value)} error={errors.password} />
          <PasswordStrength id="su-strength" value={pw} />
        </div>
        <div>
          <label className="flex cursor-pointer items-start gap-2.5 text-sm"><input type="checkbox" name="terms" aria-invalid={!!errors.terms} className="accent-primary mt-0.5 size-4" /><span>I agree to the <a href="#" className={link}>Terms</a> and <a href="#" className={link}>Privacy Policy</a>.</span></label>
          {errors.terms && <p className="text-destructive mt-1.5 text-sm">{errors.terms}</p>}
        </div>
        <AuthButton type="submit" busy={busy}>{busy ? "Creating account…" : "Create account"}</AuthButton>
      </form>
      <p className="text-muted-foreground mt-6 text-center text-sm">Already have an account? <a href={links["sign-in"]} className={link}>Sign in</a></p>
    </>
  )
}

/** Verify with a 6-digit code: auto-submits when complete, and offers a resend after a countdown. */
function VerifyForm({ hrefs }: FormProps) {
  const links = h(hrefs)
  const [code, setCode] = React.useState("")
  const [state, setState] = React.useState<"idle" | "error" | "ok">("idle")
  const [wait, setWait] = React.useState(30)
  React.useEffect(() => {
    if (wait <= 0) return
    const id = setTimeout(() => setWait((w) => w - 1), 1000)
    return () => clearTimeout(id)
  }, [wait])
  if (state === "ok") return <Success title="Email verified" action={<a className={link} href={links.onboarding}>Set up your workspace →</a>}>Thanks. Your account is ready.</Success>
  return (
    <>
      <span className="bg-primary/10 text-primary mb-5 flex size-12 items-center justify-center rounded-2xl"><MailCheck className="size-6" aria-hidden="true" /></span>
      <Heading title="Enter your code">We sent a 6-digit code to <span className="text-foreground font-medium">mina@fieldnote.example</span>. Try 123456.</Heading>
      <label htmlFor="vf-code" className="sr-only">6-digit code</label>
      <InputOTP id="vf-code" maxLength={6} value={code} aria-invalid={state === "error" || undefined} aria-describedby="vf-status" pattern="^[0-9]*$" onChange={(v) => { setCode(v); setState("idle") }} onComplete={(v) => setState(v === "123456" ? "ok" : "error")} containerClassName="justify-center">
        <InputOTPGroup>{[0, 1, 2].map((i) => <InputOTPSlot key={i} index={i} invalid={state === "error"} className="size-12 text-lg" />)}</InputOTPGroup>
        <InputOTPSeparator />
        <InputOTPGroup>{[3, 4, 5].map((i) => <InputOTPSlot key={i} index={i} invalid={state === "error"} className="size-12 text-lg" />)}</InputOTPGroup>
      </InputOTP>
      <p id="vf-status" role="status" className={cn("mt-4 text-center text-sm", state === "error" ? "text-destructive" : "text-muted-foreground")}>{state === "error" ? "That code isn’t right. Check it and try again." : "Paste the code, or type it in."}</p>
      <p className="text-muted-foreground mt-6 text-center text-sm">
        Didn’t get it?{" "}
        <button type="button" disabled={wait > 0} onClick={() => { setWait(30); setCode(""); setState("idle") }} className={cn(link, "disabled:text-muted-foreground disabled:no-underline")}>{wait > 0 ? `Resend in ${wait}s` : "Resend the code"}</button>
      </p>
      <p className="mt-3 text-center text-sm"><a href={links["sign-in"]} className="text-muted-foreground hover:text-foreground inline-flex items-center gap-1.5"><ArrowLeft className="size-3.5 rtl:rotate-180" aria-hidden="true" />Back to sign in</a></p>
    </>
  )
}

/** Ask for a reset link. */
function ForgotForm({ hrefs }: FormProps) {
  const links = h(hrefs)
  const fake = useFake()
  const [error, setError] = React.useState("")
  const [busy, setBusy] = React.useState(false)
  const [sent, setSent] = React.useState<string | null>(null)
  function submit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault()
    const mail = String(new FormData(e.currentTarget).get("email") ?? "").trim()
    if (!emailOk(mail)) {
      setError("Enter an email like you@company.com.")
      ;(e.currentTarget.elements.namedItem("email") as HTMLElement | null)?.focus()
      return
    }
    setError("")
    setBusy(true)
    fake(() => { setBusy(false); setSent(mail) })
  }
  if (sent) return <Success title="Check your email" action={<button type="button" onClick={() => setSent(null)} className={link}>Use a different email</button>}>If an account exists for <span className="text-foreground font-medium">{sent}</span>, a reset link is on its way. It works for one hour.</Success>
  return (
    <>
      <Heading title="Forgot your password?">No problem. Tell us your email and we’ll send a link to choose a new one.</Heading>
      <form onSubmit={submit} noValidate className="space-y-4">
        <AuthField id="fp-email" name="email" type="email" label="Email" autoComplete="email" error={error} />
        <AuthButton type="submit" busy={busy}>{busy ? "Sending…" : "Send reset link"}</AuthButton>
      </form>
      <p className="mt-6 text-center text-sm"><a href={links["sign-in"]} className="text-muted-foreground hover:text-foreground inline-flex items-center gap-1.5"><ArrowLeft className="size-3.5 rtl:rotate-180" aria-hidden="true" />Back to sign in</a></p>
    </>
  )
}

/** Choose a new password, with confirmation. */
function ResetForm({ hrefs }: FormProps) {
  const links = h(hrefs)
  const fake = useFake()
  const [pw, setPw] = React.useState("")
  const [errors, setErrors] = React.useState<{ password?: string; confirm?: string }>({})
  const [busy, setBusy] = React.useState(false)
  const [done, setDone] = React.useState(false)
  function submit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault()
    const confirm = String(new FormData(e.currentTarget).get("confirm") ?? "")
    const next: typeof errors = {}
    if (!passwordRules.every((r) => r.test(pw))) next.password = "Meet every rule below to continue."
    else if (confirm !== pw) next.confirm = "The passwords don’t match."
    setErrors(next)
    if (next.password || next.confirm) {
      ;(e.currentTarget.elements.namedItem(next.password ? "password" : "confirm") as HTMLElement | null)?.focus()
      return
    }
    setBusy(true)
    fake(() => { setBusy(false); setDone(true) })
  }
  if (done) return <Success title="Password updated" action={<a className={link} href={links["sign-in"]}>Sign in with your new password →</a>}>You’ve been signed out everywhere else for your safety.</Success>
  return (
    <>
      <Heading title="Choose a new password">Pick something you haven’t used before.</Heading>
      <form onSubmit={submit} noValidate className="space-y-4">
        <div>
          <AuthPassword id="rs-pass" name="password" label="New password" autoComplete="new-password" value={pw} onChange={(e) => setPw(e.target.value)} error={errors.password} />
          <PasswordStrength id="rs-strength" value={pw} />
        </div>
        <AuthPassword id="rs-confirm" name="confirm" label="Confirm password" autoComplete="new-password" error={errors.confirm} />
        <AuthButton type="submit" busy={busy}>{busy ? "Saving…" : "Update password"}</AuthButton>
      </form>
    </>
  )
}

/** Accept an invitation to a team. */
function InviteForm({ hrefs }: FormProps) {
  const fake = useFake()
  const [pw, setPw] = React.useState("")
  const [error, setError] = React.useState("")
  const [busy, setBusy] = React.useState(false)
  const [done, setDone] = React.useState(false)
  function submit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault()
    if (!passwordRules.every((r) => r.test(pw))) {
      setError("Meet every rule below to continue.")
      ;(e.currentTarget.elements.namedItem("password") as HTMLElement | null)?.focus()
      return
    }
    setError("")
    setBusy(true)
    fake(() => { setBusy(false); setDone(true) })
  }
  const links = h(hrefs)
  if (done) return <Success title="Welcome to Fieldnote Goods" action={<a className={link} href={links.onboarding}>Finish setting up →</a>}>You’ve joined as an editor.</Success>
  return (
    <>
      <div className="bg-secondary mb-6 flex items-center gap-3 rounded-2xl p-3.5">
        <span className="bg-chart-4/25 flex size-11 items-center justify-center rounded-xl text-sm font-semibold" aria-hidden="true">FG</span>
        <p className="text-sm text-pretty"><span className="font-semibold">Maya Okafor</span> invited you to join <span className="font-semibold">Fieldnote Goods</span> as an editor.</p>
      </div>
      <Heading title="Accept your invitation">Create a password for <span className="text-foreground font-medium">dev@fieldnote.example</span>.</Heading>
      <form onSubmit={submit} noValidate className="space-y-4">
        <AuthField id="iv-name" name="name" label="Full name" defaultValue="Dev Patel" autoComplete="name" />
        <div>
          <AuthPassword id="iv-pass" name="password" autoComplete="new-password" value={pw} onChange={(e) => setPw(e.target.value)} error={error} />
          <PasswordStrength id="iv-strength" value={pw} />
        </div>
        <AuthButton type="submit" busy={busy}>{busy ? "Joining…" : "Join Fieldnote Goods"}</AuthButton>
      </form>
    </>
  )
}

const steps = ["You", "Workspace", "Team"]

/** Three short steps to set up a workspace, with progress and a slug that follows the name. */
function OnboardingForm({ hrefs }: FormProps) {
  const links = h(hrefs)
  const [step, setStep] = React.useState(0)
  const [name, setName] = React.useState("Mina Kovac")
  const [role, setRole] = React.useState("Design")
  const [ws, setWs] = React.useState("Fieldnote Goods")
  const [invites, setInvites] = React.useState("")
  const [error, setError] = React.useState("")
  const [done, setDone] = React.useState(false)
  const head = React.useRef<HTMLHeadingElement>(null)
  const slug = ws.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "")
  const emails = invites.split(/[\s,]+/).filter(Boolean)
  const roles = ["Design", "Engineering", "Product", "Marketing", "Operations"]

  function next() {
    if (step === 0 && !name.trim()) return setError("Tell us your name.")
    if (step === 1 && !ws.trim()) return setError("Give your workspace a name.")
    if (step === 2 && emails.some((m) => !emailOk(m))) return setError("One of those doesn’t look like an email address.")
    setError("")
    if (step === 2) return setDone(true)
    setStep((s) => s + 1)
    requestAnimationFrame(() => head.current?.focus())
  }
  if (done) return <Success title="You’re all set" action={<a className={link} href="#">Open {ws || "your workspace"} →</a>}>{emails.length > 0 ? `We sent ${emails.length} invitation${emails.length === 1 ? "" : "s"}.` : "You can invite your team any time."}</Success>
  return (
    <>
      <ol className="mb-7 flex items-center gap-2" aria-label="Setup progress">
        {steps.map((s, i) => (
          <li key={s} aria-current={i === step ? "step" : undefined} className="flex flex-1 items-center gap-2 text-xs font-medium">
            <span className={cn("flex size-6 shrink-0 items-center justify-center rounded-full border text-[11px]", i < step ? "bg-primary text-primary-foreground border-transparent" : i === step ? "border-primary text-primary" : "text-muted-foreground")}>{i < step ? <Check className="size-3.5" aria-label="done" /> : i + 1}</span>
            <span className={i === step ? "text-foreground" : "text-muted-foreground"}>{s}</span>
            {i < steps.length - 1 && <span className="bg-border h-px flex-1" aria-hidden="true" />}
          </li>
        ))}
      </ol>
      <header className="mb-6">
        <h1 ref={head} tabIndex={-1} className="text-[1.75rem] leading-tight font-semibold tracking-[-0.03em] outline-none">{step === 0 ? "Tell us about you" : step === 1 ? "Name your workspace" : "Invite your team"}</h1>
        <p className="text-muted-foreground mt-2 text-pretty">{step === 0 ? "So your teammates know who you are." : step === 1 ? "You can change this any time." : "They’ll get an email with a link to join. You can skip this."}</p>
      </header>
      <div className="space-y-4">
        {step === 0 && (
          <>
            <AuthField id="ob-name" label="Full name" value={name} onChange={(e) => setName(e.target.value)} autoComplete="name" error={error} />
            <fieldset><legend className="text-sm font-medium">Your team</legend>
              <div className="mt-2 flex flex-wrap gap-2">
                {roles.map((r) => <label key={r} className={cn("focus-within:ring-ring/50 cursor-pointer rounded-full border px-3.5 py-1.5 text-sm transition-colors focus-within:ring-[3px]", role === r ? "bg-primary text-primary-foreground border-transparent" : "hover:bg-accent")}><input type="radio" name="role" value={r} checked={role === r} onChange={() => setRole(r)} className="sr-only" />{r}</label>)}
              </div>
            </fieldset>
          </>
        )}
        {step === 1 && (
          <>
            <AuthField id="ob-ws" label="Workspace name" value={ws} onChange={(e) => setWs(e.target.value)} error={error} hint={slug ? `Your address: ${slug}.keystone.app` : undefined} />
          </>
        )}
        {step === 2 && (
          <div>
            <label htmlFor="ob-inv" className="text-sm font-medium">Email addresses</label>
            <textarea id="ob-inv" rows={3} value={invites} onChange={(e) => setInvites(e.target.value)} placeholder="dev@company.com, priya@company.com" aria-invalid={!!error} className="bg-background focus-visible:ring-ring/50 placeholder:text-muted-foreground mt-1.5 w-full resize-none rounded-xl border p-3.5 text-[15px] outline-none focus-visible:ring-[3px] aria-[invalid=true]:border-destructive" />
            <p className={cn("mt-1.5 text-sm", error ? "text-destructive" : "text-muted-foreground")} role="status">{error || (emails.length ? `${emails.length} ${emails.length === 1 ? "person" : "people"} will be invited` : "Separate addresses with commas or spaces.")}</p>
          </div>
        )}
      </div>
      <div className="mt-7 flex items-center justify-between gap-3">
        {step > 0 ? <button type="button" onClick={() => { setError(""); setStep((s) => s - 1) }} className="text-muted-foreground hover:text-foreground focus-visible:ring-ring/50 inline-flex items-center gap-1.5 rounded-lg px-1 text-sm outline-none focus-visible:ring-[3px]"><ArrowLeft className="size-4 rtl:rotate-180" aria-hidden="true" />Back</button> : <button type="button" onClick={() => setDone(true)} className="text-muted-foreground hover:text-foreground focus-visible:ring-ring/50 rounded px-1 text-sm outline-none focus-visible:ring-[3px]">Skip for now</button>}
        <AuthButton type="button" onClick={next} className="w-auto px-6">{step === 2 ? (emails.length ? "Send invitations" : "Finish") : "Continue"}{step < 2 && <ArrowRight className="size-4 rtl:rotate-180" aria-hidden="true" />}</AuthButton>
      </div>
      <p className="text-muted-foreground mt-6 flex items-center justify-center gap-1.5 text-xs"><ShieldCheck className="size-3.5" aria-hidden="true" />Step {step + 1} of {steps.length}</p>
    </>
  )
}

export { ForgotForm, InviteForm, OnboardingForm, ResetForm, SignInForm, SignUpForm, VerifyForm, type FormProps }
```

### `registry/ballmac/components/templates/auth-kit/auth-kit-pages.tsx`

```tsx
// Ballmac UI: Auth Kit pages. https://ui.ballmac.com/templates/template-auth-kit
"use client"

import * as React from "react"

import { SegmentedControl, SegmentedControlItem } from "@/components/ballmac/segmented-control"
import { ForgotForm, InviteForm, OnboardingForm, ResetForm, SignInForm, SignUpForm, VerifyForm } from "@/components/ballmac/templates/auth-kit/auth-kit-forms"
import { AuthFrame, authDefaultHrefs, type AuthHrefs, type AuthLayout, type AuthPage } from "@/components/ballmac/templates/auth-kit/auth-kit-theme"
import { cn } from "@/lib/utils"

const forms: Record<AuthPage, React.ComponentType<{ hrefs?: Partial<AuthHrefs> }>> = {
  "sign-in": SignInForm,
  "sign-up": SignUpForm,
  verify: VerifyForm,
  forgot: ForgotForm,
  reset: ResetForm,
  invite: InviteForm,
  onboarding: OnboardingForm,
}

const labels: Record<AuthPage, string> = { "sign-in": "Sign in", "sign-up": "Sign up", verify: "Verify", forgot: "Forgot", reset: "Reset", invite: "Invite", onboarding: "Onboarding" }
const defaultLayout: Record<AuthPage, AuthLayout> = { "sign-in": "split", "sign-up": "centered", verify: "glass", forgot: "centered", reset: "centered", invite: "split", onboarding: "glass" }

type AuthKitPageProps = React.ComponentProps<"div"> & {
  /** Which page to show. */
  page: AuthPage
  /** How the page is composed. Defaults to the layout that suits the page. */
  layout?: AuthLayout
  /** Override where pages live (used by previews). */
  hrefs?: Partial<AuthHrefs>
}

/** One page of the auth flow in a chosen layout. */
function AuthKitPage({ page, layout, hrefs, ...props }: AuthKitPageProps) {
  const Form = forms[page]
  return (
    <AuthFrame layout={layout ?? defaultLayout[page]} {...props}>
      <Form hrefs={hrefs} />
    </AuthFrame>
  )
}

type AuthKitPlaygroundProps = React.ComponentProps<"div"> & {
  /** Page shown first. */
  defaultPage?: AuthPage
  /** Layout shown first. */
  defaultLayout?: AuthLayout
}

/** Every page and every layout in one place: pick a flow and a layout from the bar and see the result. */
function AuthKitPlayground({ defaultPage = "sign-in", defaultLayout: initialLayout = "split", ...props }: AuthKitPlaygroundProps) {
  const [page, setPage] = React.useState<AuthPage>(defaultPage)
  const [layout, setLayout] = React.useState<AuthLayout>(initialLayout)
  const Form = forms[page]
  // Links between forms switch the page here instead of navigating away.
  const hrefs = Object.fromEntries((Object.keys(forms) as AuthPage[]).map((k) => [k, `#${k}`])) as AuthHrefs
  React.useEffect(() => {
    const onHash = () => {
      const k = window.location.hash.slice(1) as AuthPage
      if (k in forms) setPage(k)
    }
    window.addEventListener("hashchange", onHash)
    return () => window.removeEventListener("hashchange", onHash)
  }, [])
  return (
    <AuthFrame layout={layout} {...props}>
      <Form key={page} hrefs={hrefs} />
      <div role="region" aria-label="Auth Kit controls" className="bg-popover/90 fixed inset-x-3 top-3 z-50 mx-auto flex max-w-3xl flex-col gap-3 rounded-2xl border p-3 shadow-[0_20px_50px_-12px_rgb(0_0_0/0.35)] backdrop-blur-xl sm:flex-row sm:items-center">
        <div role="group" aria-label="Page" className="flex gap-1 overflow-x-auto [scrollbar-width:none]">
          {(Object.keys(forms) as AuthPage[]).map((k) => (
            <button key={k} type="button" aria-pressed={page === k} onClick={() => setPage(k)} className={cn("focus-visible:ring-ring/50 h-8 shrink-0 rounded-lg px-3 text-[13px] font-medium outline-none transition-colors focus-visible:ring-[3px]", page === k ? "bg-primary text-primary-foreground" : "hover:bg-accent text-muted-foreground")}>{labels[k]}</button>
          ))}
        </div>
        <SegmentedControl aria-label="Layout" value={layout} onValueChange={(v) => setLayout(v as AuthLayout)} size="sm" className="sm:ms-auto">
          <SegmentedControlItem value="centered">Centered</SegmentedControlItem>
          <SegmentedControlItem value="split">Split</SegmentedControlItem>
          <SegmentedControlItem value="glass">Glass</SegmentedControlItem>
        </SegmentedControl>
      </div>
    </AuthFrame>
  )
}

export { AuthKitPage, AuthKitPlayground, authDefaultHrefs, type AuthKitPageProps, type AuthKitPlaygroundProps }
```

### `registry/ballmac/app/auth/sign-in/page.tsx`

```tsx
// Ballmac UI: Auth Kit template route. https://ui.ballmac.com/templates/template-auth-kit
import type { Metadata } from "next"

import { AuthKitPage } from "@/components/ballmac/templates/auth-kit/auth-kit-pages"

export const metadata: Metadata = {
  title: "Sign in",
  description: "Welcome back. Sign in to continue.",
}

export default function Page() {
  return <AuthKitPage page="sign-in" />
}
```

### `registry/ballmac/app/auth/sign-up/page.tsx`

```tsx
// Ballmac UI: Auth Kit template route. https://ui.ballmac.com/templates/template-auth-kit
import type { Metadata } from "next"

import { AuthKitPage } from "@/components/ballmac/templates/auth-kit/auth-kit-pages"

export const metadata: Metadata = {
  title: "Create your account",
  description: "Free for 14 days. No card needed.",
}

export default function Page() {
  return <AuthKitPage page="sign-up" />
}
```

### `registry/ballmac/app/auth/verify/page.tsx`

```tsx
// Ballmac UI: Auth Kit template route. https://ui.ballmac.com/templates/template-auth-kit
import type { Metadata } from "next"

import { AuthKitPage } from "@/components/ballmac/templates/auth-kit/auth-kit-pages"

export const metadata: Metadata = {
  title: "Verify your email",
  description: "Enter the 6-digit code we sent you.",
}

export default function Page() {
  return <AuthKitPage page="verify" />
}
```

### `registry/ballmac/app/auth/forgot/page.tsx`

```tsx
// Ballmac UI: Auth Kit template route. https://ui.ballmac.com/templates/template-auth-kit
import type { Metadata } from "next"

import { AuthKitPage } from "@/components/ballmac/templates/auth-kit/auth-kit-pages"

export const metadata: Metadata = {
  title: "Forgot your password?",
  description: "Get a link to choose a new password.",
}

export default function Page() {
  return <AuthKitPage page="forgot" />
}
```

### `registry/ballmac/app/auth/reset/page.tsx`

```tsx
// Ballmac UI: Auth Kit template route. https://ui.ballmac.com/templates/template-auth-kit
import type { Metadata } from "next"

import { AuthKitPage } from "@/components/ballmac/templates/auth-kit/auth-kit-pages"

export const metadata: Metadata = {
  title: "Choose a new password",
  description: "Pick a password you haven't used before.",
}

export default function Page() {
  return <AuthKitPage page="reset" />
}
```

### `registry/ballmac/app/auth/invite/page.tsx`

```tsx
// Ballmac UI: Auth Kit template route. https://ui.ballmac.com/templates/template-auth-kit
import type { Metadata } from "next"

import { AuthKitPage } from "@/components/ballmac/templates/auth-kit/auth-kit-pages"

export const metadata: Metadata = {
  title: "Accept your invitation",
  description: "Join your team's workspace.",
}

export default function Page() {
  return <AuthKitPage page="invite" />
}
```

### `registry/ballmac/app/auth/onboarding/page.tsx`

```tsx
// Ballmac UI: Auth Kit template route. https://ui.ballmac.com/templates/template-auth-kit
import type { Metadata } from "next"

import { AuthKitPage } from "@/components/ballmac/templates/auth-kit/auth-kit-pages"

export const metadata: Metadata = {
  title: "Set up your workspace",
  description: "Three short steps to get started.",
}

export default function Page() {
  return <AuthKitPage page="onboarding" />
}
```
