{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "cloud-backup-prompt",
  "title": "Cloud Backup Prompt",
  "author": "Satchmo",
  "description": "Dialog that prompts users to set up encrypted cloud backup after first sign-in, with password strength validation and confirmation flow",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "dialog",
    "input",
    "label"
  ],
  "files": [
    {
      "path": "registry/new-york/blocks/cloud-backup-prompt/index.tsx",
      "content": "\"use client\"\n\nimport {\n  CloudBackupPromptUi,\n} from \"./cloud-backup-prompt-ui\"\nimport {\n  useCloudBackup,\n} from \"./use-cloud-backup\"\n\n// ---------------------------------------------------------------------------\n// Re-exports\n// ---------------------------------------------------------------------------\n\nexport { CloudBackupPromptUi } from \"./cloud-backup-prompt-ui\"\nexport type { CloudBackupPromptUiProps } from \"./cloud-backup-prompt-ui\"\nexport { useCloudBackup } from \"./use-cloud-backup\"\nexport type {\n  UseCloudBackupOptions,\n  UseCloudBackupReturn,\n} from \"./use-cloud-backup\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface CloudBackupPromptProps {\n  /** Whether the dialog is open */\n  open: boolean\n  /** Called when the dialog open state changes */\n  onOpenChange: (open: boolean) => void\n  /** Callback that performs the actual encryption + upload */\n  onSave: (password: string) => Promise<void>\n  /** Called when \"Remind Me Later\" is clicked */\n  onRemindLater?: () => void\n  /** Called on successful save */\n  onSuccess?: () => void\n  /** Called on error */\n  onError?: (error: Error) => void\n  /** Minimum password length (default: 8) */\n  minPasswordLength?: number\n  /** Optional CSS class for the dialog content */\n  className?: string\n}\n\n// ---------------------------------------------------------------------------\n// Composed component\n// ---------------------------------------------------------------------------\n\n/**\n * A dialog that prompts users to set up encrypted cloud backup after\n * first sign-in.\n *\n * The block is agnostic about how encryption and upload are performed --\n * the parent provides an `onSave` callback that receives the chosen\n * password and handles the rest.\n *\n * @example\n * ```tsx\n * import { CloudBackupPrompt } from \"@/components/blocks/cloud-backup-prompt\"\n *\n * function App() {\n *   const [open, setOpen] = useState(true)\n *\n *   return (\n *     <CloudBackupPrompt\n *       open={open}\n *       onOpenChange={setOpen}\n *       onSave={async (password) => {\n *         const backup = localStorage.getItem(\"encrypted-backup\")\n *         if (!backup) throw new Error(\"No backup found\")\n *         await uploadBackup(backup, password)\n *       }}\n *       onSuccess={() => console.log(\"Backup saved\")}\n *     />\n *   )\n * }\n * ```\n */\nexport function CloudBackupPrompt({\n  open,\n  onOpenChange,\n  onSave,\n  onRemindLater,\n  onSuccess,\n  onError,\n  minPasswordLength = 8,\n  className,\n}: CloudBackupPromptProps) {\n  const hook = useCloudBackup({\n    onSave,\n    onSuccess,\n    onError,\n    minPasswordLength,\n  })\n\n  return (\n    <CloudBackupPromptUi\n      open={open}\n      onOpenChange={onOpenChange}\n      hook={hook}\n      onRemindLater={onRemindLater}\n      minPasswordLength={minPasswordLength}\n      className={className}\n    />\n  )\n}\n",
      "type": "registry:block",
      "target": "~/components/blocks/cloud-backup-prompt/index.tsx"
    },
    {
      "path": "registry/new-york/blocks/cloud-backup-prompt/cloud-backup-prompt-ui.tsx",
      "content": "\"use client\"\n\nimport { useCallback, useEffect, useRef } from \"react\"\nimport {\n  AlertCircle,\n  CheckCircle2,\n  CloudUpload,\n  KeyRound,\n  Loader2,\n  ShieldCheck,\n} from \"lucide-react\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n} from \"@/components/ui/dialog\"\nimport { Input } from \"@/components/ui/input\"\nimport { Label } from \"@/components/ui/label\"\nimport { cn } from \"@/lib/utils\"\nimport type { UseCloudBackupReturn } from \"./use-cloud-backup\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface CloudBackupPromptUiProps {\n  /** Whether the dialog is open */\n  open: boolean\n  /** Called when the dialog open state changes */\n  onOpenChange: (open: boolean) => void\n  /** Hook return values */\n  hook: UseCloudBackupReturn\n  /** Called when \"Remind Me Later\" is clicked */\n  onRemindLater?: () => void\n  /** Minimum password length for helper text */\n  minPasswordLength: number\n  /** Optional CSS class for the dialog content */\n  className?: string\n}\n\n// ---------------------------------------------------------------------------\n// Strength bar\n// ---------------------------------------------------------------------------\n\nconst STRENGTH_COLORS: Record<number, string> = {\n  0: \"bg-muted\",\n  1: \"bg-destructive\",\n  2: \"bg-destructive/60\",\n  3: \"bg-primary/60\",\n  4: \"bg-primary\",\n}\n\ninterface StrengthBarProps {\n  strength: number\n  label: string\n}\n\nfunction StrengthBar({ strength, label }: StrengthBarProps) {\n  return (\n    <div className=\"flex flex-col gap-1.5\">\n      <div className=\"flex gap-1\">\n        {([1, 2, 3, 4] as const).map((level) => (\n          <div\n            key={level}\n            className={cn(\n              \"h-1.5 flex-1 rounded-full transition-colors duration-200\",\n              strength >= level\n                ? STRENGTH_COLORS[strength]\n                : \"bg-muted\",\n            )}\n            aria-hidden=\"true\"\n          />\n        ))}\n      </div>\n      {label && (\n        <p className=\"text-xs text-muted-foreground\">{label}</p>\n      )}\n    </div>\n  )\n}\n\n// ---------------------------------------------------------------------------\n// Main UI\n// ---------------------------------------------------------------------------\n\nexport function CloudBackupPromptUi({\n  open,\n  onOpenChange,\n  hook,\n  onRemindLater,\n  minPasswordLength,\n  className,\n}: CloudBackupPromptUiProps) {\n  const passwordRef = useRef<HTMLInputElement>(null)\n\n  // Auto-focus password field when dialog opens\n  useEffect(() => {\n    if (open && hook.status === \"idle\") {\n      // Small delay to let the dialog animate in\n      const timer = setTimeout(() => {\n        passwordRef.current?.focus()\n      }, 100)\n      return () => clearTimeout(timer)\n    }\n  }, [open, hook.status])\n\n  const handleRemindLater = useCallback(() => {\n    onRemindLater?.()\n    onOpenChange(false)\n  }, [onRemindLater, onOpenChange])\n\n  const handleKeyDown = useCallback(\n    (e: React.KeyboardEvent<HTMLInputElement>) => {\n      if (e.key === \"Enter\" && hook.validation.isValid) {\n        e.preventDefault()\n        void hook.save()\n      }\n    },\n    [hook],\n  )\n\n  const isBusy = hook.status === \"saving\"\n\n  // ---- Success state ----\n  if (hook.status === \"success\") {\n    return (\n      <Dialog open={open} onOpenChange={onOpenChange}>\n        <DialogContent className={cn(\"sm:max-w-md\", className)}>\n          <div className=\"flex flex-col items-center gap-4 py-6\">\n            <div className=\"flex size-16 items-center justify-center rounded-full bg-primary/10\">\n              <CheckCircle2\n                className=\"size-8 text-primary\"\n                aria-hidden=\"true\"\n              />\n            </div>\n            <div className=\"flex flex-col items-center gap-1 text-center\">\n              <p className=\"text-lg font-semibold\">Backup Enabled</p>\n              <p className=\"text-sm text-muted-foreground\">\n                Your wallet is now protected with encrypted cloud backup.\n                You can restore it from any device.\n              </p>\n            </div>\n          </div>\n          <DialogFooter>\n            <Button\n              className=\"w-full\"\n              onClick={() => onOpenChange(false)}\n            >\n              Done\n            </Button>\n          </DialogFooter>\n        </DialogContent>\n      </Dialog>\n    )\n  }\n\n  // ---- Idle / saving / error state ----\n  return (\n    <Dialog open={open} onOpenChange={onOpenChange}>\n      <DialogContent className={cn(\"sm:max-w-md\", className)}>\n        <DialogHeader>\n          <div className=\"mb-2 flex items-center gap-3\">\n            <div className=\"rounded-full bg-primary/10 p-3\">\n              <CloudUpload className=\"size-6 text-primary\" aria-hidden=\"true\" />\n            </div>\n            <DialogTitle className=\"text-xl\">\n              Secure Your Wallet\n            </DialogTitle>\n          </div>\n          <DialogDescription className=\"text-left\">\n            Create an encryption password to protect your wallet backup\n            in the cloud. You will need this password to restore your\n            wallet on another device.\n          </DialogDescription>\n        </DialogHeader>\n\n        <div className=\"space-y-4 py-2\">\n          {/* Features */}\n          <div className=\"space-y-3\">\n            <div className=\"flex gap-3\">\n              <ShieldCheck\n                className=\"mt-0.5 size-5 shrink-0 text-muted-foreground\"\n                aria-hidden=\"true\"\n              />\n              <div>\n                <p className=\"text-sm font-medium\">End-to-end encrypted</p>\n                <p className=\"text-xs text-muted-foreground\">\n                  Your backup is encrypted locally. We never see your\n                  private keys.\n                </p>\n              </div>\n            </div>\n            <div className=\"flex gap-3\">\n              <KeyRound\n                className=\"mt-0.5 size-5 shrink-0 text-muted-foreground\"\n                aria-hidden=\"true\"\n              />\n              <div>\n                <p className=\"text-sm font-medium\">Password-protected</p>\n                <p className=\"text-xs text-muted-foreground\">\n                  Only you can decrypt your backup with your chosen\n                  password.\n                </p>\n              </div>\n            </div>\n          </div>\n\n          {/* Password input */}\n          <div className=\"space-y-2\">\n            <Label htmlFor=\"cloud-backup-password\">Password</Label>\n            <Input\n              ref={passwordRef}\n              id=\"cloud-backup-password\"\n              type=\"password\"\n              placeholder={`At least ${minPasswordLength} characters`}\n              value={hook.password}\n              onChange={(e) => hook.setPassword(e.target.value)}\n              onKeyDown={handleKeyDown}\n              disabled={isBusy}\n              autoComplete=\"new-password\"\n            />\n            {hook.password.length > 0 && (\n              <StrengthBar\n                strength={hook.validation.strength}\n                label={hook.validation.strengthLabel}\n              />\n            )}\n          </div>\n\n          {/* Confirm password */}\n          <div className=\"space-y-2\">\n            <Label htmlFor=\"cloud-backup-confirm\">Confirm Password</Label>\n            <Input\n              id=\"cloud-backup-confirm\"\n              type=\"password\"\n              placeholder=\"Re-enter your password\"\n              value={hook.confirmPassword}\n              onChange={(e) => hook.setConfirmPassword(e.target.value)}\n              onKeyDown={handleKeyDown}\n              disabled={isBusy}\n              autoComplete=\"new-password\"\n            />\n            {hook.confirmPassword.length > 0 &&\n              !hook.validation.matchesConfirmation && (\n                <p className=\"text-xs text-destructive\">\n                  Passwords do not match\n                </p>\n              )}\n          </div>\n\n          {/* Error */}\n          {hook.error && (\n            <div className=\"flex items-start gap-3 rounded-md border border-destructive/20 bg-destructive/5 p-3\">\n              <AlertCircle className=\"mt-0.5 size-4 flex-shrink-0 text-destructive\" />\n              <div className=\"flex flex-col gap-1\">\n                <p className=\"text-sm font-medium\">Backup failed</p>\n                <p className=\"text-xs text-muted-foreground\">\n                  {hook.error.message}\n                </p>\n              </div>\n            </div>\n          )}\n        </div>\n\n        <DialogFooter className=\"flex-col-reverse gap-2 sm:flex-row\">\n          <Button\n            variant=\"outline\"\n            disabled={isBusy}\n            onClick={handleRemindLater}\n          >\n            Remind Me Later\n          </Button>\n          <Button\n            disabled={isBusy || !hook.validation.isValid}\n            onClick={() => {\n              void hook.save()\n            }}\n            aria-busy={isBusy}\n          >\n            {isBusy ? (\n              <>\n                <Loader2\n                  className=\"animate-spin\"\n                  data-icon=\"inline-start\"\n                  aria-hidden=\"true\"\n                />\n                Encrypting...\n              </>\n            ) : (\n              \"Enable Backup\"\n            )}\n          </Button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  )\n}\n",
      "type": "registry:component",
      "target": "~/components/blocks/cloud-backup-prompt/cloud-backup-prompt-ui.tsx"
    },
    {
      "path": "registry/new-york/blocks/cloud-backup-prompt/use-cloud-backup.ts",
      "content": "import { useCallback, useMemo, useState } from \"react\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Possible states of the cloud backup flow */\ntype CloudBackupStatus = \"idle\" | \"saving\" | \"success\" | \"error\"\n\n/** Password validation result */\ninterface PasswordValidation {\n  /** Whether the password meets all requirements */\n  isValid: boolean\n  /** Whether the password meets the minimum length */\n  meetsLength: boolean\n  /** Whether the password and confirmation match */\n  matchesConfirmation: boolean\n  /** Strength score from 0 to 4 */\n  strength: PasswordStrength\n  /** Human-readable strength label */\n  strengthLabel: string\n}\n\n/** Password strength as a numeric score (0 = empty, 4 = strong) */\ntype PasswordStrength = 0 | 1 | 2 | 3 | 4\n\n/** Options for the useCloudBackup hook */\nexport interface UseCloudBackupOptions {\n  /** Callback that performs the actual encryption + upload */\n  onSave: (password: string) => Promise<void>\n  /** Called on successful save */\n  onSuccess?: () => void\n  /** Called on error */\n  onError?: (error: Error) => void\n  /** Minimum password length (default: 8) */\n  minPasswordLength?: number\n}\n\n/** Return type of the useCloudBackup hook */\nexport interface UseCloudBackupReturn {\n  /** Current status of the backup flow */\n  status: CloudBackupStatus\n  /** Password value */\n  password: string\n  /** Confirmation password value */\n  confirmPassword: string\n  /** Set the password value */\n  setPassword: (value: string) => void\n  /** Set the confirmation password value */\n  setConfirmPassword: (value: string) => void\n  /** Password validation result */\n  validation: PasswordValidation\n  /** Error from the save operation */\n  error: Error | null\n  /** Execute the save operation */\n  save: () => Promise<void>\n  /** Reset the hook state */\n  reset: () => void\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nconst STRENGTH_LABELS: Record<PasswordStrength, string> = {\n  0: \"\",\n  1: \"Weak\",\n  2: \"Fair\",\n  3: \"Good\",\n  4: \"Strong\",\n}\n\n/**\n * Calculate password strength on a 0-4 scale.\n *\n * Criteria:\n * - Length >= minLength  (+1)\n * - Has uppercase and lowercase  (+1)\n * - Has at least one digit  (+1)\n * - Has at least one special character  (+1)\n */\nfunction calculateStrength(\n  password: string,\n  minLength: number,\n): PasswordStrength {\n  if (password.length === 0) return 0\n\n  let score = 0\n  if (password.length >= minLength) score++\n  if (/[a-z]/.test(password) && /[A-Z]/.test(password)) score++\n  if (/\\d/.test(password)) score++\n  if (/[^a-zA-Z0-9]/.test(password)) score++\n\n  return score as PasswordStrength\n}\n\n// ---------------------------------------------------------------------------\n// Hook\n// ---------------------------------------------------------------------------\n\nexport function useCloudBackup({\n  onSave,\n  onSuccess,\n  onError,\n  minPasswordLength = 8,\n}: UseCloudBackupOptions): UseCloudBackupReturn {\n  const [status, setStatus] = useState<CloudBackupStatus>(\"idle\")\n  const [password, setPassword] = useState(\"\")\n  const [confirmPassword, setConfirmPassword] = useState(\"\")\n  const [error, setError] = useState<Error | null>(null)\n\n  const validation = useMemo<PasswordValidation>(() => {\n    const meetsLength = password.length >= minPasswordLength\n    const matchesConfirmation =\n      confirmPassword.length > 0 && password === confirmPassword\n    const strength = calculateStrength(password, minPasswordLength)\n\n    return {\n      isValid: meetsLength && matchesConfirmation,\n      meetsLength,\n      matchesConfirmation,\n      strength,\n      strengthLabel: STRENGTH_LABELS[strength],\n    }\n  }, [password, confirmPassword, minPasswordLength])\n\n  const save = useCallback(async () => {\n    if (!validation.isValid) return\n\n    setStatus(\"saving\")\n    setError(null)\n\n    try {\n      await onSave(password)\n      setStatus(\"success\")\n      onSuccess?.()\n    } catch (e) {\n      const err = e instanceof Error ? e : new Error(String(e))\n      setError(err)\n      setStatus(\"error\")\n      onError?.(err)\n    }\n  }, [validation.isValid, onSave, password, onSuccess, onError])\n\n  const reset = useCallback(() => {\n    setStatus(\"idle\")\n    setPassword(\"\")\n    setConfirmPassword(\"\")\n    setError(null)\n  }, [])\n\n  return {\n    status,\n    password,\n    confirmPassword,\n    setPassword,\n    setConfirmPassword,\n    validation,\n    error,\n    save,\n    reset,\n  }\n}\n",
      "type": "registry:component",
      "target": "~/components/blocks/cloud-backup-prompt/use-cloud-backup.ts"
    }
  ],
  "type": "registry:block"
}