{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "auth-two-factor-setup",
  "title": "Auth Two Factor Setup",
  "description": "Set up two-factor authentication with QR code and backup codes.",
  "dependencies": [
    "next"
  ],
  "registryDependencies": [
    "button",
    "card",
    "field",
    "input-group",
    "separator",
    "switch"
  ],
  "files": [
    {
      "path": "registry/new-york/blocks/auth/auth-two-factor-setup.tsx",
      "content": "\"use client\";\n\nimport {\n  CheckCircle2,\n  Copy,\n  Download,\n  Eye,\n  EyeOff,\n  Loader2,\n  QrCode,\n  RefreshCw,\n  Shield,\n  ShieldCheck,\n  ShieldOff,\n} from \"lucide-react\";\nimport Image from \"next/image\";\nimport { useCallback, 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  FieldDescription,\n  FieldError,\n  FieldLabel,\n} from \"@/registry/new-york/ui/field\";\nimport {\n  InputGroup,\n  InputGroupAddon,\n  InputGroupButton,\n  InputGroupInput,\n} from \"@/registry/new-york/ui/input-group\";\nimport { Separator } from \"@/registry/new-york/ui/separator\";\nimport { Switch } from \"@/registry/new-york/ui/switch\";\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\ninterface EnabledStateHeaderProps {\n  className?: string;\n}\n\nfunction EnabledStateHeader({ className }: EnabledStateHeaderProps) {\n  return (\n    <div\n      className={cn(\n        \"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\",\n        className\n      )}\n    >\n      <div className=\"min-w-0 flex-1\">\n        <CardTitle className=\"flex flex-wrap items-center gap-2\">\n          <ShieldCheck\n            aria-hidden=\"true\"\n            className=\"size-5 shrink-0 text-primary\"\n          />\n          <span className=\"wrap-break-word\">Two-factor authentication</span>\n        </CardTitle>\n        <CardDescription className=\"wrap-break-word\">\n          Your account is protected with 2FA\n        </CardDescription>\n      </div>\n      <div className=\"flex shrink-0 items-center gap-2\">\n        <span className=\"whitespace-nowrap text-muted-foreground text-sm\">\n          Enabled\n        </span>\n        <Switch checked={true} disabled />\n      </div>\n    </div>\n  );\n}\n\nfunction ActiveStatusCard() {\n  return (\n    <div className=\"rounded-lg border bg-muted/50 p-4\">\n      <div className=\"flex items-start gap-3\">\n        <ShieldCheck\n          aria-hidden=\"true\"\n          className=\"size-5 shrink-0 text-primary\"\n        />\n        <div className=\"flex min-w-0 flex-1 flex-col gap-2\">\n          <p className=\"wrap-break-word font-medium text-sm\">2FA is active</p>\n          <p className=\"wrap-break-word text-muted-foreground text-xs\">\n            Your account is protected with two-factor authentication.\n            You&apos;ll need to enter a code from your authenticator app when\n            signing in.\n          </p>\n        </div>\n      </div>\n    </div>\n  );\n}\n\ninterface BackupCodeItemProps {\n  code: string;\n  index: number;\n  copiedIndex: number | null;\n  onCopy: (code: string, index: number) => void;\n}\n\nfunction BackupCodeItem({\n  code,\n  index,\n  copiedIndex,\n  onCopy,\n}: BackupCodeItemProps) {\n  return (\n    <div className=\"flex min-w-0 items-center justify-between gap-2 font-mono text-sm\">\n      <span className=\"min-w-0 break-all\">{code}</span>\n      <Button\n        aria-label={`Copy backup code ${index + 1}`}\n        className=\"min-h-[32px] min-w-[32px] shrink-0 touch-manipulation\"\n        onClick={() => onCopy(code, index)}\n        size=\"icon-sm\"\n        type=\"button\"\n        variant=\"ghost\"\n      >\n        {copiedIndex === index ? (\n          <CheckCircle2 aria-hidden=\"true\" className=\"size-3.5 text-primary\" />\n        ) : (\n          <Copy aria-hidden=\"true\" className=\"size-3.5\" />\n        )}\n      </Button>\n    </div>\n  );\n}\n\ninterface BackupCodesListProps {\n  codes: string[];\n  copiedIndex: number | null;\n  onCopy: (code: string, index: number) => void;\n}\n\nfunction BackupCodesList({ codes, copiedIndex, onCopy }: BackupCodesListProps) {\n  return (\n    <div className=\"grid grid-cols-1 gap-2 rounded-lg border bg-background p-4 sm:grid-cols-2\">\n      {codes.map((code, index) => (\n        <BackupCodeItem\n          code={code}\n          copiedIndex={copiedIndex}\n          index={index}\n          key={index}\n          onCopy={onCopy}\n        />\n      ))}\n    </div>\n  );\n}\n\ninterface BackupCodesActionsProps {\n  onDownload: () => void;\n  onRegenerate?: () => void;\n}\n\nfunction BackupCodesActions({\n  onDownload,\n  onRegenerate,\n}: BackupCodesActionsProps) {\n  return (\n    <div className=\"flex flex-col gap-2 sm:flex-row\">\n      <Button\n        className=\"min-h-[44px] w-full touch-manipulation sm:min-h-[32px] sm:w-auto\"\n        onClick={onDownload}\n        type=\"button\"\n        variant=\"outline\"\n      >\n        <Download aria-hidden=\"true\" className=\"size-4\" />\n        Download codes\n      </Button>\n      {onRegenerate && (\n        <Button\n          className=\"min-h-[44px] w-full touch-manipulation sm:min-h-[32px] sm:w-auto\"\n          onClick={onRegenerate}\n          type=\"button\"\n          variant=\"outline\"\n        >\n          <RefreshCw aria-hidden=\"true\" className=\"size-4\" />\n          Regenerate codes\n        </Button>\n      )}\n    </div>\n  );\n}\n\ninterface BackupCodesSectionProps {\n  codes: string[];\n  showCodes: boolean;\n  onToggleShow: () => void;\n  copiedIndex: number | null;\n  onCopy: (code: string, index: number) => void;\n  onDownload: () => void;\n  onRegenerate?: () => void;\n}\n\nfunction BackupCodesSection({\n  codes,\n  showCodes,\n  onToggleShow,\n  copiedIndex,\n  onCopy,\n  onDownload,\n  onRegenerate,\n}: BackupCodesSectionProps) {\n  return (\n    <div className=\"flex flex-col gap-3\">\n      <div className=\"flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between\">\n        <div className=\"min-w-0 flex-1\">\n          <h3 className=\"wrap-break-word font-medium text-sm\">Backup codes</h3>\n          <p className=\"wrap-break-word text-muted-foreground text-xs\">\n            Save these codes in a safe place. You can use them to access your\n            account if you lose your device.\n          </p>\n        </div>\n        <Button\n          className=\"min-h-[44px] w-full touch-manipulation sm:min-h-[32px] sm:w-auto\"\n          onClick={onToggleShow}\n          type=\"button\"\n          variant=\"outline\"\n        >\n          {showCodes ? \"Hide\" : \"Show\"} codes\n        </Button>\n      </div>\n      {showCodes && (\n        <BackupCodesList\n          codes={codes}\n          copiedIndex={copiedIndex}\n          onCopy={onCopy}\n        />\n      )}\n      <BackupCodesActions onDownload={onDownload} onRegenerate={onRegenerate} />\n    </div>\n  );\n}\n\ninterface PasswordFieldProps {\n  value: string;\n  onChange: (value: string) => void;\n  showPassword: boolean;\n  onToggleShow: () => void;\n  error?: string;\n}\n\nfunction PasswordField({\n  value,\n  onChange,\n  showPassword,\n  onToggleShow,\n  error,\n}: PasswordFieldProps) {\n  return (\n    <Field data-invalid={!!error}>\n      <FieldLabel htmlFor=\"disable-2fa-password\">\n        Password\n        <span aria-label=\"required\" className=\"text-destructive\">\n          *\n        </span>\n      </FieldLabel>\n      <FieldContent>\n        <InputGroup aria-invalid={!!error}>\n          <InputGroupAddon>\n            <Shield aria-hidden=\"true\" className=\"size-4\" />\n          </InputGroupAddon>\n          <InputGroupInput\n            aria-describedby={error ? \"disable-2fa-password-error\" : undefined}\n            aria-invalid={!!error}\n            autoComplete=\"current-password\"\n            id=\"disable-2fa-password\"\n            name=\"password\"\n            onChange={(e) => onChange(e.target.value)}\n            placeholder=\"Enter your password…\"\n            required\n            type={showPassword ? \"text\" : \"password\"}\n            value={value}\n          />\n          <InputGroupButton\n            aria-label={showPassword ? \"Hide password\" : \"Show password\"}\n            className=\"min-h-[32px] min-w-[32px] touch-manipulation\"\n            onClick={(e) => {\n              e.preventDefault();\n              onToggleShow();\n            }}\n            type=\"button\"\n          >\n            {showPassword ? (\n              <EyeOff aria-hidden=\"true\" className=\"size-4\" />\n            ) : (\n              <Eye aria-hidden=\"true\" className=\"size-4\" />\n            )}\n          </InputGroupButton>\n        </InputGroup>\n        {error && (\n          <FieldError id=\"disable-2fa-password-error\">{error}</FieldError>\n        )}\n      </FieldContent>\n    </Field>\n  );\n}\n\ninterface DisableButtonProps {\n  isLoading: boolean;\n  disabled: boolean;\n  onClick: () => void;\n}\n\nfunction DisableButton({ isLoading, disabled, onClick }: DisableButtonProps) {\n  return (\n    <Button\n      aria-busy={isLoading}\n      className=\"min-h-[44px] w-full touch-manipulation sm:min-h-[32px]\"\n      data-loading={isLoading}\n      disabled={disabled}\n      onClick={onClick}\n      type=\"button\"\n      variant=\"destructive\"\n    >\n      {isLoading ? (\n        <>\n          <Loader2 aria-hidden=\"true\" className=\"size-4 animate-spin\" />\n          Disabling…\n        </>\n      ) : (\n        <>\n          <ShieldOff aria-hidden=\"true\" className=\"size-4\" />\n          Disable 2FA\n        </>\n      )}\n    </Button>\n  );\n}\n\ninterface DisableSectionProps {\n  password: string;\n  onPasswordChange: (value: string) => void;\n  showPassword: boolean;\n  onToggleShowPassword: () => void;\n  error?: string;\n  isLoading: boolean;\n  onDisable: () => void;\n}\n\nfunction DisableSection({\n  password,\n  onPasswordChange,\n  showPassword,\n  onToggleShowPassword,\n  error,\n  isLoading,\n  onDisable,\n}: DisableSectionProps) {\n  return (\n    <div className=\"flex flex-col gap-4\">\n      <div className=\"min-w-0\">\n        <h3 className=\"wrap-break-word font-medium text-destructive text-sm\">\n          Disable two-factor authentication\n        </h3>\n        <p className=\"wrap-break-word text-muted-foreground text-xs\">\n          You&apos;ll need to enter your password to disable 2FA.\n        </p>\n      </div>\n      <PasswordField\n        error={error}\n        onChange={onPasswordChange}\n        onToggleShow={onToggleShowPassword}\n        showPassword={showPassword}\n        value={password}\n      />\n      <DisableButton\n        disabled={isLoading || !password.trim()}\n        isLoading={isLoading}\n        onClick={onDisable}\n      />\n    </div>\n  );\n}\n\nfunction SetupHeader() {\n  return (\n    <>\n      <CardTitle className=\"flex flex-wrap items-center gap-2\">\n        <span className=\"wrap-break-word\">\n          Set up two-factor authentication\n        </span>\n      </CardTitle>\n      <CardDescription className=\"wrap-break-word\">\n        Add an extra layer of security to your account\n      </CardDescription>\n    </>\n  );\n}\n\nfunction SetupInstructions() {\n  return (\n    <div className=\"rounded-lg border bg-muted/50 p-4\">\n      <p className=\"wrap-break-word text-muted-foreground text-sm\">\n        Scan the QR code with your authenticator app (like Google Authenticator,\n        Authy, or 1Password) to set up two-factor authentication.\n      </p>\n    </div>\n  );\n}\n\ninterface QRCodeDisplayProps {\n  qrCodeUrl: string;\n}\n\nfunction QRCodeDisplay({ qrCodeUrl }: QRCodeDisplayProps) {\n  return (\n    <div className=\"flex flex-col items-center gap-4 rounded-lg border bg-background p-4 sm:p-6\">\n      <div className=\"relative aspect-square w-full max-w-[192px] rounded-lg border bg-white p-3 sm:size-48 sm:p-4\">\n        <Image\n          alt=\"QR code for two-factor authentication\"\n          className=\"object-contain p-4\"\n          fill\n          sizes=\"(max-width: 640px) 192px, 192px\"\n          src={qrCodeUrl}\n        />\n      </div>\n      <p className=\"text-center text-muted-foreground text-xs\">\n        Scan this QR code with your authenticator app\n      </p>\n    </div>\n  );\n}\n\ninterface SecretKeyFieldProps {\n  secretKey: string;\n  onCopy: () => void;\n}\n\nfunction SecretKeyField({ secretKey, onCopy }: SecretKeyFieldProps) {\n  return (\n    <Field>\n      <FieldLabel>Manual entry key</FieldLabel>\n      <FieldContent>\n        <InputGroup>\n          <InputGroupAddon>\n            <QrCode aria-hidden=\"true\" className=\"size-4\" />\n          </InputGroupAddon>\n          <InputGroupInput readOnly type=\"text\" value={secretKey} />\n          <InputGroupButton\n            aria-label=\"Copy secret key\"\n            className=\"min-h-[32px] min-w-[32px] touch-manipulation\"\n            onClick={onCopy}\n            type=\"button\"\n          >\n            <Copy aria-hidden=\"true\" className=\"size-4\" />\n          </InputGroupButton>\n        </InputGroup>\n        <FieldDescription>\n          If you can&apos;t scan the QR code, enter this key manually in your\n          authenticator app\n        </FieldDescription>\n      </FieldContent>\n    </Field>\n  );\n}\n\ninterface RegenerateButtonProps {\n  onRegenerate: () => void;\n}\n\nfunction RegenerateButton({ onRegenerate }: RegenerateButtonProps) {\n  return (\n    <Button\n      className=\"min-h-[44px] w-full touch-manipulation sm:min-h-[32px] sm:w-auto\"\n      onClick={onRegenerate}\n      type=\"button\"\n      variant=\"outline\"\n    >\n      <RefreshCw aria-hidden=\"true\" className=\"size-4\" />\n      Generate new QR code\n    </Button>\n  );\n}\n\ninterface EnableButtonProps {\n  isLoading: boolean;\n  onClick?: () => void;\n}\n\nfunction EnableButton({ isLoading, onClick }: EnableButtonProps) {\n  return (\n    <div className=\"flex flex-col gap-2\">\n      <Button\n        aria-busy={isLoading}\n        className=\"min-h-[44px] w-full touch-manipulation sm:min-h-[32px]\"\n        data-loading={isLoading}\n        disabled={isLoading}\n        onClick={onClick}\n        type=\"button\"\n      >\n        {isLoading ? (\n          <>\n            <Loader2 aria-hidden=\"true\" className=\"size-4 animate-spin\" />\n            Enabling 2FA…\n          </>\n        ) : (\n          <>\n            <ShieldCheck aria-hidden=\"true\" className=\"size-4\" />\n            Enable 2FA\n          </>\n        )}\n      </Button>\n      <p className=\"text-center text-muted-foreground text-xs\">\n        After enabling, you&apos;ll need to verify with a code from your\n        authenticator app\n      </p>\n    </div>\n  );\n}\n\nfunction downloadBackupCodes(codes: string[]): void {\n  if (codes.length === 0) return;\n  const content = codes.join(\"\\n\");\n  const blob = new Blob([content], { type: \"text/plain\" });\n  const url = URL.createObjectURL(blob);\n  const a = document.createElement(\"a\");\n  a.href = url;\n  a.download = \"backup-codes.txt\";\n  document.body.appendChild(a);\n  a.click();\n  document.body.removeChild(a);\n  URL.revokeObjectURL(url);\n}\n\nexport interface AuthTwoFactorSetupProps {\n  isEnabled?: boolean;\n  qrCodeUrl?: string;\n  secretKey?: string;\n  backupCodes?: string[];\n  onEnable?: () => void;\n  onDisable?: (password: string) => void;\n  onGenerateBackupCodes?: () => void;\n  onRegenerateSecret?: () => void;\n  className?: string;\n  isLoading?: boolean;\n  errors?: {\n    password?: string;\n    general?: string;\n  };\n}\n\nexport default function AuthTwoFactorSetup({\n  isEnabled = false,\n  qrCodeUrl,\n  secretKey,\n  backupCodes = [],\n  onEnable,\n  onDisable,\n  onGenerateBackupCodes,\n  onRegenerateSecret,\n  className,\n  isLoading = false,\n  errors,\n}: AuthTwoFactorSetupProps) {\n  const [password, setPassword] = useState(\"\");\n  const [showPassword, setShowPassword] = useState(false);\n  const [showBackupCodes, setShowBackupCodes] = useState(false);\n  const [copiedIndex, setCopiedIndex] = useState<number | null>(null);\n\n  const handleCopyCode = useCallback(async (code: string, index: number) => {\n    try {\n      await navigator.clipboard.writeText(code);\n      setCopiedIndex(index);\n      setTimeout(() => setCopiedIndex(null), 2000);\n    } catch {}\n  }, []);\n\n  const handleCopySecret = useCallback(async () => {\n    if (secretKey) {\n      try {\n        await navigator.clipboard.writeText(secretKey);\n      } catch {}\n    }\n  }, [secretKey]);\n\n  const handleDisable = useCallback(() => {\n    if (!password.trim()) return;\n    onDisable?.(password);\n    setPassword(\"\");\n  }, [password, onDisable]);\n\n  const handleDownloadBackupCodes = useCallback(() => {\n    downloadBackupCodes(backupCodes);\n  }, [backupCodes]);\n\n  const handleToggleShowPassword = useCallback(() => {\n    setShowPassword((prev) => !prev);\n  }, []);\n\n  const handleToggleShowBackupCodes = useCallback(() => {\n    setShowBackupCodes((prev) => !prev);\n  }, []);\n\n  if (isEnabled) {\n    return (\n      <Card className={cn(\"w-full max-w-sm shadow-xs\", className)}>\n        <CardHeader>\n          <EnabledStateHeader />\n        </CardHeader>\n        <CardContent>\n          <div className=\"flex flex-col gap-6\">\n            {errors?.general && <ErrorAlert message={errors.general} />}\n\n            <div className=\"flex flex-col gap-4\">\n              <ActiveStatusCard />\n\n              {backupCodes.length > 0 && (\n                <BackupCodesSection\n                  codes={backupCodes}\n                  copiedIndex={copiedIndex}\n                  onCopy={handleCopyCode}\n                  onDownload={handleDownloadBackupCodes}\n                  onRegenerate={onGenerateBackupCodes}\n                  onToggleShow={handleToggleShowBackupCodes}\n                  showCodes={showBackupCodes}\n                />\n              )}\n\n              <Separator />\n\n              <DisableSection\n                error={errors?.password}\n                isLoading={isLoading}\n                onDisable={handleDisable}\n                onPasswordChange={setPassword}\n                onToggleShowPassword={handleToggleShowPassword}\n                password={password}\n                showPassword={showPassword}\n              />\n            </div>\n          </div>\n        </CardContent>\n      </Card>\n    );\n  }\n\n  return (\n    <Card className={cn(\"w-full max-w-sm shadow-xs\", className)}>\n      <CardHeader>\n        <SetupHeader />\n      </CardHeader>\n      <CardContent>\n        <div className=\"flex flex-col gap-6\">\n          {errors?.general && <ErrorAlert message={errors.general} />}\n\n          <div className=\"flex flex-col gap-4\">\n            <SetupInstructions />\n\n            {qrCodeUrl && <QRCodeDisplay qrCodeUrl={qrCodeUrl} />}\n\n            {secretKey && (\n              <SecretKeyField onCopy={handleCopySecret} secretKey={secretKey} />\n            )}\n\n            {onRegenerateSecret && (\n              <RegenerateButton onRegenerate={onRegenerateSecret} />\n            )}\n\n            <Separator />\n\n            <EnableButton isLoading={isLoading} onClick={onEnable} />\n          </div>\n        </div>\n      </CardContent>\n    </Card>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "block",
    "auth"
  ],
  "type": "registry:ui"
}