# Verify 1: one-time code with resend

> A code-entry screen: masked email, six slots that accept typing and pasting, automatic submit on the last digit, shake and alert on a wrong code, a resend countdown and an animated success state.

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

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

## Install

```bash
npx shadcn@latest add https://ui.ballmac.com/r/verify-1.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`, `motion`
- Registry items: `utils`, `https://ui.ballmac.com/r/button.json`, `https://ui.ballmac.com/r/input-otp.json`, `https://ui.ballmac.com/r/use-resource.json`

## Files

### `registry/ballmac/components/blocks/verify-1/verify-1.tsx`

```tsx
// Ballmac UI: Verify 1. https://ui.ballmac.com/blocks/verify-1
"use client"

import * as React from "react"
import { Check, ShieldCheck } from "lucide-react"
import { motion, useReducedMotion } from "motion/react"

import { Button, buttonVariants } from "@/components/ballmac/button"
import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from "@/components/ballmac/input-otp"
import { formPhase, type FormStateProps } from "@/lib/ballmac/use-resource"
import { cn } from "@/lib/utils"

type Verify1Props = Omit<React.ComponentProps<"section">, "title" | "onSubmit"> & FormStateProps & {
  /** A code to start with, for example from a link in the email. */
  defaultCode?: string
  /** Where the code was sent. Shown partly masked. */
  email?: string
  /** Heading. */
  title?: string
  /** Digits in the code. */
  length?: number
  /**
   * Called with the full code, as soon as the last digit is typed or pasted. Throw to show "wrong code"; resolve to show success.
   * Without it, any code except all zeros is accepted after a short pause, so you can try the states.
   */
  onVerify?: (code: string) => void | Promise<void>
  /** Called by "Resend code". Without it the countdown simply restarts. */
  onResend?: () => void | Promise<void>
  /** Seconds before "Resend code" works again. */
  resendAfter?: number
  /** Called by "Use a different email". Pass null to hide it. */
  onChangeEmail?: (() => void) | null
  /** Where the success button goes. */
  continueHref?: string
}

function maskEmail(email: string) {
  const [name, domain] = email.split("@")
  if (!domain) return email
  return `${name.slice(0, 1)}${"•".repeat(Math.max(name.length - 1, 2))}@${domain}`
}

function Verify1({
  email = "jordan@acme.com",
  title = "Enter your code",
  length = 6,
  onVerify,
  onResend,
  resendAfter = 30,
  onChangeEmail = () => {},
  continueHref = "#",
  defaultCode = "",
  state: stateProp,
  errorState,
  className,
  ...props
}: Verify1Props) {
  const reduce = useReducedMotion()
  const [code, setCode] = React.useState(defaultCode)
  const [internal, setInternal] = React.useState<"idle" | "checking" | "wrong" | "done">("idle")
  // a `state` prop drives the form; without it the form runs by itself
  const phase = formPhase(stateProp)
  const state = phase === undefined ? internal : phase === "sending" ? "checking" : phase === "error" ? "wrong" : phase
  const [wait, setWait] = React.useState(resendAfter)
  const [resent, setResent] = React.useState(false)
  const headingRef = React.useRef<HTMLHeadingElement>(null)
  const otpRef = React.useRef<HTMLInputElement>(null)

  React.useEffect(() => {
    if (wait <= 0) return
    const id = window.setTimeout(() => setWait((w) => w - 1), 1000)
    return () => window.clearTimeout(id)
  }, [wait])

  async function check(value: string) {
    setInternal("checking")
    try {
      if (onVerify) await onVerify(value)
      else {
        await new Promise((r) => setTimeout(r, 800))
        if (/^0+$/.test(value)) throw new Error("wrong")
      }
      setInternal("done")
      window.setTimeout(() => headingRef.current?.focus(), 0)
    } catch {
      setInternal("wrong")
      setCode("")
      window.setTimeout(() => otpRef.current?.focus(), 0)
    }
  }

  async function resend() {
    setResent(true)
    setCode("")
    setInternal("idle")
    setWait(resendAfter)
    await onResend?.()
    otpRef.current?.focus()
  }

  const half = Math.ceil(length / 2)
  const slots = Array.from({ length }, (_, i) => i)
  return (
    <section data-slot="verify-1" className={cn("flex min-h-[36rem] items-center justify-center px-4 py-14", className)} {...props}>
      <div className="bg-card w-full max-w-md rounded-3xl border p-7 text-center shadow-[0_30px_80px_-50px_rgb(0_0_0/0.4)] sm:p-9">
        {state === "done" ? (
          <div className="flex flex-col items-center py-4">
            <motion.span
              aria-hidden="true"
              initial={reduce ? false : { scale: 0.5, opacity: 0 }}
              animate={{ scale: 1, opacity: 1 }}
              transition={{ type: "spring", stiffness: 300, damping: 18 }}
              className="bg-chart-2/15 flex size-14 items-center justify-center rounded-full"
            >
              <Check className="size-7" />
            </motion.span>
            <h1 ref={headingRef} tabIndex={-1} className="mt-6 text-2xl font-semibold tracking-[-0.03em] outline-none">You’re verified</h1>
            <p role="status" className="text-muted-foreground mt-2 max-w-xs text-pretty">Thanks for confirming it’s you. Your account is ready to use.</p>
            <a href={continueHref} className={buttonVariants({ size: "lg", shape: "pill", className: "mt-8 w-full" })}>Continue</a>
          </div>
        ) : (
          <>
            <span aria-hidden="true" className="bg-muted mx-auto flex size-12 items-center justify-center rounded-2xl"><ShieldCheck className="size-6" /></span>
            <h1 ref={headingRef} tabIndex={-1} className="mt-6 text-2xl font-semibold tracking-[-0.03em] outline-none">{title}</h1>
            <p className="text-muted-foreground mt-2 text-pretty">We sent a {length}-digit code to <span className="text-foreground font-medium">{maskEmail(email)}</span>. It expires in 10 minutes.</p>

            <motion.div
              className="mt-8 flex justify-center"
              animate={state === "wrong" && !reduce ? { x: [0, -8, 8, -6, 6, -3, 0] } : { x: 0 }}
              transition={{ duration: 0.4 }}
            >
              <InputOTP
                ref={otpRef}
                maxLength={length}
                value={code}
                autoFocus
                disabled={state === "checking"}
                aria-label={`${length}-digit verification code`}
                aria-invalid={state === "wrong" || undefined}
                aria-describedby="verify-1-status"
                onChange={(v) => { setCode(v); if (state === "wrong") setInternal("idle") }}
                onComplete={check}
              >
                <InputOTPGroup>{slots.slice(0, half).map((i) => <InputOTPSlot key={i} index={i} invalid={state === "wrong"} />)}</InputOTPGroup>
                <InputOTPSeparator />
                <InputOTPGroup>{slots.slice(half).map((i) => <InputOTPSlot key={i} index={i} invalid={state === "wrong"} />)}</InputOTPGroup>
              </InputOTP>
            </motion.div>

            <p id="verify-1-status" role={state === "wrong" ? "alert" : "status"} className={cn("mt-4 min-h-5 text-sm", state === "wrong" ? "text-destructive" : "text-muted-foreground")}>
              {state === "checking" && "Checking your code…"}
              {state === "wrong" && (errorState?.description ?? "That code isn’t right. Check the email and try again.")}
              {state === "idle" && resent && "A new code is on its way."}
            </p>

            <p className="text-muted-foreground mt-6 text-sm">
              {wait > 0 ? <>Didn’t get it? You can resend in <span className="tabular-nums">{wait}</span>s.</> : "Didn’t get the code?"}
            </p>
            <div className="mt-1 flex justify-center gap-2">
              <Button variant="ghost" size="sm" disabled={wait > 0 || state === "checking"} onClick={resend}>Resend code</Button>
              {onChangeEmail && <Button variant="ghost" size="sm" onClick={onChangeEmail}>Use a different email</Button>}
            </div>
          </>
        )}
      </div>
    </section>
  )
}

export { Verify1, type Verify1Props }
```
