{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "auth-two-factor-verify",
  "title": "Auth Two Factor Verify",
  "description": "Verify two-factor authentication code or recovery code.",
  "registryDependencies": [
    "button",
    "card",
    "field",
    "input-group",
    "input-otp",
    "separator"
  ],
  "files": [
    {
      "path": "registry/new-york/blocks/auth/auth-two-factor-verify.tsx",
      "content": "\"use client\";\n\nimport { Loader2, RefreshCw, ShieldCheck } from \"lucide-react\";\nimport { useCallback, useEffect, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\nimport { Button } from \"@/registry/new-york/ui/button\";\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,\n  CardTitle,\n} from \"@/registry/new-york/ui/card\";\nimport {\n  Field,\n  FieldContent,\n  FieldError,\n  FieldLabel,\n} from \"@/registry/new-york/ui/field\";\nimport {\n  InputGroup,\n  InputGroupAddon,\n  InputGroupInput,\n} from \"@/registry/new-york/ui/input-group\";\nimport {\n  InputOTP,\n  InputOTPGroup,\n  InputOTPSlot,\n} from \"@/registry/new-york/ui/input-otp\";\nimport { Separator } from \"@/registry/new-york/ui/separator\";\n\ninterface ErrorAlertProps {\n  message: string;\n}\n\nfunction ErrorAlert({ message }: ErrorAlertProps) {\n  return (\n    <div\n      aria-live=\"polite\"\n      className=\"rounded-lg border border-destructive/50 bg-destructive/10 p-3 text-destructive text-sm\"\n      role=\"alert\"\n    >\n      {message}\n    </div>\n  );\n}\n\nfunction Header() {\n  return (\n    <>\n      <CardTitle className=\"flex items-center gap-2\">\n        Verify two-factor authentication\n      </CardTitle>\n      <CardDescription>\n        Enter the 6-digit code from your authenticator app\n      </CardDescription>\n    </>\n  );\n}\n\ninterface RecoveryCodeFieldProps {\n  value: string;\n  onChange: (value: string) => void;\n  error?: string;\n}\n\nfunction RecoveryCodeField({ value, onChange, error }: RecoveryCodeFieldProps) {\n  return (\n    <Field data-invalid={!!error}>\n      <FieldLabel htmlFor=\"recovery-code\">\n        Recovery code\n        <span aria-label=\"required\" className=\"text-destructive\">\n          *\n        </span>\n      </FieldLabel>\n      <FieldContent>\n        <InputGroup aria-invalid={!!error}>\n          <InputGroupAddon>\n            <ShieldCheck aria-hidden=\"true\" className=\"size-4\" />\n          </InputGroupAddon>\n          <InputGroupInput\n            aria-describedby={error ? \"recovery-code-error\" : undefined}\n            aria-invalid={!!error}\n            autoComplete=\"one-time-code\"\n            id=\"recovery-code\"\n            name=\"recoveryCode\"\n            onChange={(e) => onChange(e.target.value)}\n            placeholder=\"Enter recovery code…\"\n            required\n            type=\"text\"\n            value={value}\n          />\n        </InputGroup>\n        {error && <FieldError id=\"recovery-code-error\">{error}</FieldError>}\n      </FieldContent>\n    </Field>\n  );\n}\n\ninterface VerifyRecoveryCodeButtonProps {\n  isLoading: boolean;\n  disabled: boolean;\n}\n\nfunction VerifyRecoveryCodeButton({\n  isLoading,\n  disabled,\n}: VerifyRecoveryCodeButtonProps) {\n  return (\n    <Button\n      aria-busy={isLoading}\n      className=\"min-h-[44px] w-full touch-manipulation sm:min-h-[32px] sm:w-auto\"\n      data-loading={isLoading}\n      disabled={disabled}\n      type=\"submit\"\n    >\n      {isLoading ? (\n        <>\n          <Loader2 aria-hidden=\"true\" className=\"size-4 animate-spin\" />\n          Verifying…\n        </>\n      ) : (\n        <>\n          <ShieldCheck aria-hidden=\"true\" className=\"size-4\" />\n          Verify recovery code\n        </>\n      )}\n    </Button>\n  );\n}\n\ninterface SwitchToAuthenticatorButtonProps {\n  onSwitch: () => void;\n}\n\nfunction SwitchToAuthenticatorButton({\n  onSwitch,\n}: SwitchToAuthenticatorButtonProps) {\n  return (\n    <Button\n      className=\"min-h-[44px] w-full touch-manipulation sm:min-h-[32px] sm:w-auto\"\n      onClick={onSwitch}\n      type=\"button\"\n      variant=\"outline\"\n    >\n      Use authenticator code\n    </Button>\n  );\n}\n\ninterface RecoveryCodeFormProps {\n  recoveryCode: string;\n  onRecoveryCodeChange: (value: string) => void;\n  error?: string;\n  isLoading: boolean;\n  onSubmit: (e: React.FormEvent) => void;\n  onSwitchToAuthenticator: () => void;\n}\n\nfunction RecoveryCodeForm({\n  recoveryCode,\n  onRecoveryCodeChange,\n  error,\n  isLoading,\n  onSubmit,\n  onSwitchToAuthenticator,\n}: RecoveryCodeFormProps) {\n  return (\n    <form className=\"flex flex-col gap-4\" onSubmit={onSubmit}>\n      <RecoveryCodeField\n        error={error}\n        onChange={onRecoveryCodeChange}\n        value={recoveryCode}\n      />\n      <div className=\"flex flex-col gap-2 sm:flex-row\">\n        <VerifyRecoveryCodeButton\n          disabled={isLoading || !recoveryCode.trim()}\n          isLoading={isLoading}\n        />\n        <SwitchToAuthenticatorButton onSwitch={onSwitchToAuthenticator} />\n      </div>\n    </form>\n  );\n}\n\ninterface ResendButtonProps {\n  cooldown: number;\n  isLoading: boolean;\n  onResend: () => void;\n}\n\nfunction ResendButton({ cooldown, isLoading, onResend }: ResendButtonProps) {\n  return (\n    <button\n      className=\"min-h-[32px] touch-manipulation self-start rounded-sm hover:text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 sm:self-auto\"\n      disabled={cooldown > 0 || isLoading}\n      onClick={onResend}\n      type=\"button\"\n    >\n      {cooldown > 0 ? (\n        `Resend in ${cooldown}s`\n      ) : (\n        <span className=\"flex items-center gap-1\">\n          <RefreshCw aria-hidden=\"true\" className=\"size-3\" />\n          Resend code\n        </span>\n      )}\n    </button>\n  );\n}\n\ninterface OTPFieldWithResendProps {\n  value: string;\n  onChange: (value: string) => void;\n  error?: string;\n  isLoading: boolean;\n  cooldown: number;\n  onResend?: () => void;\n}\n\nfunction OTPFieldWithResend({\n  value,\n  onChange,\n  error,\n  isLoading,\n  cooldown,\n  onResend,\n}: OTPFieldWithResendProps) {\n  return (\n    <Field data-invalid={!!error}>\n      <FieldLabel htmlFor=\"2fa-code\">\n        Authentication code\n        <span aria-label=\"required\" className=\"text-destructive\">\n          *\n        </span>\n      </FieldLabel>\n      <FieldContent>\n        <InputOTP\n          aria-describedby={error ? \"2fa-code-error\" : undefined}\n          aria-invalid={!!error}\n          disabled={isLoading}\n          id=\"2fa-code\"\n          maxLength={6}\n          onChange={onChange}\n          value={value}\n        >\n          <InputOTPGroup>\n            <InputOTPSlot index={0} />\n            <InputOTPSlot index={1} />\n            <InputOTPSlot index={2} />\n            <InputOTPSlot index={3} />\n            <InputOTPSlot index={4} />\n            <InputOTPSlot index={5} />\n          </InputOTPGroup>\n        </InputOTP>\n        {error && <FieldError id=\"2fa-code-error\">{error}</FieldError>}\n        <div className=\"flex flex-col gap-2 text-muted-foreground text-xs sm:flex-row sm:items-center sm:justify-between\">\n          <span>Enter the 6-digit code from your app</span>\n          {onResend && (\n            <ResendButton\n              cooldown={cooldown}\n              isLoading={isLoading}\n              onResend={onResend}\n            />\n          )}\n        </div>\n      </FieldContent>\n    </Field>\n  );\n}\n\nfunction EmailSeparator() {\n  return (\n    <div className=\"relative\">\n      <Separator />\n      <div className=\"absolute inset-0 flex items-center justify-center\">\n        <span className=\"bg-card px-2 text-muted-foreground text-xs\">Or</span>\n      </div>\n    </div>\n  );\n}\n\ninterface RecoveryCodeToggleButtonProps {\n  onToggle: () => void;\n}\n\nfunction RecoveryCodeToggleButton({ onToggle }: RecoveryCodeToggleButtonProps) {\n  return (\n    <Button\n      className=\"min-h-[44px] w-full touch-manipulation sm:min-h-[32px]\"\n      onClick={onToggle}\n      type=\"button\"\n      variant=\"ghost\"\n    >\n      Use recovery code instead\n    </Button>\n  );\n}\n\nexport interface AuthTwoFactorVerifyProps {\n  onSubmit?: (code: string) => void;\n  onRecoveryCode?: (code: string) => void;\n  onResend?: () => void;\n  className?: string;\n  isLoading?: boolean;\n  resendCooldown?: number; // seconds\n  errors?: {\n    code?: string;\n    recoveryCode?: string;\n    general?: string;\n  };\n  autoSubmit?: boolean;\n}\n\nexport default function AuthTwoFactorVerify({\n  onSubmit,\n  onRecoveryCode,\n  onResend,\n  className,\n  isLoading = false,\n  resendCooldown = 60,\n  errors,\n  autoSubmit = true,\n}: AuthTwoFactorVerifyProps) {\n  const [code, setCode] = useState(\"\");\n  const [recoveryCode, setRecoveryCode] = useState(\"\");\n  const [useRecoveryCode, setUseRecoveryCode] = useState(false);\n  const [cooldown, setCooldown] = useState(0);\n\n  useEffect(() => {\n    if (cooldown > 0) {\n      const timer = setTimeout(() => {\n        setCooldown((prev) => prev - 1);\n      }, 1000);\n      return () => clearTimeout(timer);\n    }\n  }, [cooldown]);\n\n  useEffect(() => {\n    if (autoSubmit && code.length === 6 && !isLoading && !useRecoveryCode) {\n      onSubmit?.(code);\n    }\n  }, [code, autoSubmit, isLoading, useRecoveryCode, onSubmit]);\n\n  const handleResend = useCallback(async () => {\n    if (cooldown > 0) return;\n    await onResend?.();\n    setCooldown(resendCooldown);\n  }, [cooldown, onResend, resendCooldown]);\n\n  const handleRecoveryCodeSubmit = useCallback(\n    (e: React.FormEvent) => {\n      e.preventDefault();\n      if (recoveryCode.trim()) {\n        onRecoveryCode?.(recoveryCode.trim());\n      }\n    },\n    [recoveryCode, onRecoveryCode]\n  );\n\n  const handleSwitchToAuthenticator = useCallback(() => {\n    setUseRecoveryCode(false);\n    setRecoveryCode(\"\");\n  }, []);\n\n  const handleSwitchToRecoveryCode = useCallback(() => {\n    setUseRecoveryCode(true);\n  }, []);\n\n  return (\n    <Card className={cn(\"w-full max-w-sm shadow-xs\", className)}>\n      <CardHeader>\n        <Header />\n      </CardHeader>\n      <CardContent>\n        <div className=\"flex flex-col gap-6\">\n          {errors?.general && <ErrorAlert message={errors.general} />}\n\n          {useRecoveryCode ? (\n            <RecoveryCodeForm\n              error={errors?.recoveryCode}\n              isLoading={isLoading}\n              onRecoveryCodeChange={setRecoveryCode}\n              onSubmit={handleRecoveryCodeSubmit}\n              onSwitchToAuthenticator={handleSwitchToAuthenticator}\n              recoveryCode={recoveryCode}\n            />\n          ) : (\n            <div className=\"flex flex-col gap-4\">\n              <OTPFieldWithResend\n                cooldown={cooldown}\n                error={errors?.code}\n                isLoading={isLoading}\n                onChange={setCode}\n                onResend={onResend}\n                value={code}\n              />\n\n              {onRecoveryCode && (\n                <>\n                  <EmailSeparator />\n                  <RecoveryCodeToggleButton\n                    onToggle={handleSwitchToRecoveryCode}\n                  />\n                </>\n              )}\n            </div>\n          )}\n        </div>\n      </CardContent>\n    </Card>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "block",
    "auth"
  ],
  "type": "registry:ui"
}