# Verify 1

> Two-factor code entry with resend cooldown, backup codes and lockout

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

- Page, with a live preview: https://shadcn.exchange/r/shadcnstore/verify-1
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/shadcnstore/verify-1-light-26b9f01ebbb842c9.png
- Registry item: https://shadcnstore.com/r/verify-1.json
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://shadcnstore.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`
- Registry items: `@shadcnstore/motion-confetti`, `button`, `card`, `input`, `input-otp`, `label`

## Files

### `storage/app/registry-sources/application/verification/verify-1/page.tsx`

```tsx
import { Verify1 } from "@/components/blocks/application/verification/verify-1/components/verify-1"

export default function Page() {
  return (
    <div className="w-full">
      <Verify1 />
    </div>
  )
}
```

### `storage/app/registry-sources/application/verification/verify-1/verify-1.tsx`

```tsx
'use client'

import { useEffect, useId, useState, type FormEvent } from 'react'
import { KeyRound, Loader2, ShieldCheck } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from '@/components/ui/input-otp'
import { Label } from '@/components/ui/label'
import { Confetti } from '@/components/motion/confetti'

/**
 * Demo rules: 123456 is accepted, anything else fails, and three wrong attempts lock the
 * form. Replace `verifyCode` and `verifyBackupCode` with your own endpoints; the states
 * around them (cooldown, attempts, lockout) are the part worth keeping.
 */
const acceptedCode = '123456'
const acceptedBackupCode = 'CQ4T-9WLM'
const maxAttempts = 3
const resendCooldownSeconds = 30

async function verifyCode(code: string): Promise<boolean> {
  await new Promise(resolve => setTimeout(resolve, 700))
  return code === acceptedCode
}

async function verifyBackupCode(code: string): Promise<boolean> {
  await new Promise(resolve => setTimeout(resolve, 700))
  return code.trim().toUpperCase() === acceptedBackupCode
}

export function Verify1() {
  const backupId = useId()
  const [mode, setMode] = useState<'otp' | 'backup'>('otp')
  const [code, setCode] = useState('')
  const [backup, setBackup] = useState('')
  const [pending, setPending] = useState(false)
  const [error, setError] = useState<string | null>(null)
  const [attempts, setAttempts] = useState(0)
  const [verified, setVerified] = useState(false)
  const [cooldown, setCooldown] = useState(resendCooldownSeconds)

  const locked = attempts >= maxAttempts

  useEffect(() => {
    if (cooldown <= 0) {
      return
    }

    const timer = window.setTimeout(() => setCooldown(current => current - 1), 1000)
    return () => window.clearTimeout(timer)
  }, [cooldown])

  const submit = async (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault()

    if (locked) {
      return
    }

    setPending(true)
    setError(null)

    try {
      const ok =
        mode === 'otp' ? await verifyCode(code) : await verifyBackupCode(backup)

      if (ok) {
        setVerified(true)
        return
      }

      const next = attempts + 1
      setAttempts(next)
      setCode('')
      setError(
        next >= maxAttempts
          ? 'Too many incorrect codes. Wait fifteen minutes or use a backup code.'
          : `That code is not right. ${maxAttempts - next} attempt${maxAttempts - next === 1 ? '' : 's'} left.`,
      )
    } finally {
      setPending(false)
    }
  }

  if (verified) {
    return (
      <div className='bg-muted/30 relative flex min-h-dvh w-full items-center justify-center overflow-hidden px-4 py-12'>
        {/* Fired once, over the whole field rather than the card, so the paper has somewhere
            to fall. The success is stated in the heading below; this only decorates it. */}
        <Confetti fire origin={{ x: 0.5, y: 0.5 }} />

        <Card className='relative w-full max-w-md gap-6 py-8 shadow-lg'>
          <CardContent className='flex flex-col items-center gap-4 px-8 py-8 text-center'>
            <span
              aria-hidden='true'
              className='bg-primary/10 text-primary flex size-14 items-center justify-center rounded-full'
            >
              <ShieldCheck className='size-7' />
            </span>
            <h1 className='text-2xl font-semibold tracking-tight'>Verification complete</h1>
            <p className='text-muted-foreground max-w-xs text-sm'>
              This device is trusted for thirty days. You can revoke it from security settings.
            </p>
            <Button className='mt-2'>Continue</Button>
          </CardContent>
        </Card>
      </div>
    )
  }

  return (
    <div className='bg-muted/30 flex min-h-dvh w-full items-center justify-center px-4 py-12'>
      <Card className='w-full max-w-md gap-6 py-8 shadow-lg'>
        <CardHeader className='gap-2 px-8 text-center'>
          <span
            aria-hidden='true'
            className='bg-muted text-muted-foreground mx-auto mb-3 flex size-14 items-center justify-center rounded-full'
          >
            {mode === 'otp' ? <ShieldCheck className='size-6' /> : <KeyRound className='size-6' />}
          </span>
          <CardTitle className='text-2xl font-semibold tracking-tight'>
            {mode === 'otp' ? 'Two-factor verification' : 'Use a backup code'}
          </CardTitle>
          <CardDescription>
            {mode === 'otp'
              ? 'Enter the six-digit code from your authenticator app.'
              : 'Enter one of the codes you saved when you turned on two-factor.'}
          </CardDescription>
        </CardHeader>

        <CardContent className='px-8'>
          <form onSubmit={submit} noValidate className='flex flex-col items-center gap-6'>
            {mode === 'otp' ? (
              <>
                <InputOTP
                  maxLength={6}
                  value={code}
                  onChange={setCode}
                  disabled={pending || locked}
                  aria-label='Verification code'
                  aria-invalid={error ? true : undefined}
                  aria-describedby={error ? 'verify-error' : undefined}
                >
                  <InputOTPGroup>
                    <InputOTPSlot index={0} className='size-12 text-lg' />
                    <InputOTPSlot index={1} className='size-12 text-lg' />
                    <InputOTPSlot index={2} className='size-12 text-lg' />
                  </InputOTPGroup>
                  <InputOTPSeparator />
                  <InputOTPGroup>
                    <InputOTPSlot index={3} className='size-12 text-lg' />
                    <InputOTPSlot index={4} className='size-12 text-lg' />
                    <InputOTPSlot index={5} className='size-12 text-lg' />
                  </InputOTPGroup>
                </InputOTP>
                <p className='text-muted-foreground text-xs'>Try 123456 in this demo.</p>
              </>
            ) : (
              <div className='grid w-full gap-2'>
                <Label htmlFor={backupId}>Backup code</Label>
                <Input
                  id={backupId}
                  value={backup}
                  onChange={event => setBackup(event.target.value)}
                  placeholder='XXXX-XXXX'
                  autoComplete='one-time-code'
                  className='font-mono tracking-wider'
                  aria-invalid={error ? true : undefined}
                  aria-describedby={error ? 'verify-error' : `${backupId}-hint`}
                  disabled={pending || locked}
                />
                <p id={`${backupId}-hint`} className='text-muted-foreground text-xs'>
                  {`Try ${acceptedBackupCode} in this demo. Each code works once.`}
                </p>
              </div>
            )}

            {error ? (
              <p id='verify-error' role='alert' className='text-destructive text-center text-sm'>
                {error}
              </p>
            ) : null}

            <Button
              type='submit'
              className='w-full'
              disabled={
                pending || locked || (mode === 'otp' ? code.length < 6 : backup.trim() === '')
              }
              aria-busy={pending}
            >
              {pending ? <Loader2 className='size-4 animate-spin' aria-hidden='true' /> : null}
              Verify
            </Button>

            <div className='flex w-full flex-col items-center gap-1 pt-1'>
              {mode === 'otp' ? (
                <Button
                  type='button'
                  variant='ghost'
                  size='sm'
                  disabled={cooldown > 0}
                  onClick={() => setCooldown(resendCooldownSeconds)}
                >
                  {cooldown > 0 ? `Resend code in ${cooldown}s` : 'Resend code'}
                </Button>
              ) : null}

              <Button
                type='button'
                variant='link'
                size='sm'
                className='text-muted-foreground'
                onClick={() => {
                  setMode(current => (current === 'otp' ? 'backup' : 'otp'))
                  setError(null)
                }}
              >
                {mode === 'otp' ? 'Use a backup code instead' : 'Use your authenticator app'}
              </Button>
            </div>
          </form>
        </CardContent>
      </Card>
    </div>
  )
}

export default Verify1
```
