{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "unlock-wallet",
  "title": "Unlock Wallet",
  "author": "Satchmo <https://bigblocks.dev>",
  "description": "Passphrase and biometric unlock screen with Touch ID support on macOS, passphrase fallback, failed attempt tracking, and success state",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "badge",
    "button",
    "card",
    "input",
    "label"
  ],
  "files": [
    {
      "path": "registry/new-york/blocks/unlock-wallet/index.tsx",
      "content": "\"use client\"\n\nimport { useUnlockWallet } from \"./use-unlock-wallet\"\nimport { UnlockWalletUi } from \"./unlock-wallet-ui\"\nimport type { UnlockPlatform, UnlockWalletResult } from \"./use-unlock-wallet\"\n\n// ---------------------------------------------------------------------------\n// Re-exports\n// ---------------------------------------------------------------------------\n\nexport { useUnlockWallet } from \"./use-unlock-wallet\"\nexport { UnlockWalletUi } from \"./unlock-wallet-ui\"\nexport type {\n  UnlockPlatform,\n  UnlockWalletResult,\n  UseUnlockWalletOptions,\n  UseUnlockWalletReturn,\n} from \"./use-unlock-wallet\"\nexport type { UnlockWalletUiProps } from \"./unlock-wallet-ui\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Props for the composed UnlockWallet block */\nexport interface UnlockWalletProps {\n  /** Platform determines which unlock methods are available (default: \"other\") */\n  platform?: UnlockPlatform\n  /** Application name displayed in the unlock UI (default: \"Wallet\") */\n  appName?: string\n  /** Callback to execute the unlock attempt */\n  onUnlock?: (passphrase?: string) => Promise<UnlockWalletResult>\n  /** Called on successful unlock */\n  onSuccess?: () => void\n  /** Called on error */\n  onError?: (error: Error) => void\n  /** Optional CSS class */\n  className?: string\n}\n\n// ---------------------------------------------------------------------------\n// Component\n// ---------------------------------------------------------------------------\n\n/**\n * Full Unlock Wallet block: passphrase or biometric unlock screen.\n *\n * On macOS, presents a Touch ID button first with a passphrase fallback.\n * On other platforms, presents a passphrase input directly.\n *\n * The `onUnlock` callback receives an optional passphrase string\n * (undefined for biometric) and should return `{ success, error? }`.\n *\n * @example\n * ```tsx\n * import { UnlockWallet } from \"@/components/blocks/unlock-wallet\"\n *\n * <UnlockWallet\n *   platform=\"macos\"\n *   appName=\"My Wallet\"\n *   onUnlock={async (passphrase) => {\n *     if (passphrase) {\n *       return verifyPassphrase(passphrase)\n *     }\n *     return attemptBiometric()\n *   }}\n * />\n * ```\n */\nexport function UnlockWallet({\n  platform = \"other\",\n  appName = \"Wallet\",\n  onUnlock,\n  onSuccess,\n  onError,\n  className,\n}: UnlockWalletProps) {\n  const { isLoading, error, isUnlocked, failedAttempts, execute, reset } =\n    useUnlockWallet({\n      platform,\n      appName,\n      onUnlock,\n      onSuccess,\n      onError,\n    })\n\n  return (\n    <UnlockWalletUi\n      platform={platform}\n      appName={appName}\n      isLoading={isLoading}\n      error={error}\n      isUnlocked={isUnlocked}\n      failedAttempts={failedAttempts}\n      onSubmit={execute}\n      onReset={reset}\n      className={className}\n    />\n  )\n}\n",
      "type": "registry:block",
      "target": "~/components/blocks/unlock-wallet/index.tsx"
    },
    {
      "path": "registry/new-york/blocks/unlock-wallet/unlock-wallet-ui.tsx",
      "content": "\"use client\"\n\nimport { useCallback, useState } from \"react\"\nimport {\n  AlertCircle,\n  CheckCircle2,\n  Fingerprint,\n  KeyRound,\n  Loader2,\n  Lock,\n  ShieldCheck,\n} from \"lucide-react\"\nimport { Badge } from \"@/components/ui/badge\"\nimport { Button } from \"@/components/ui/button\"\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardFooter,\n  CardHeader,\n  CardTitle,\n} from \"@/components/ui/card\"\nimport { Input } from \"@/components/ui/input\"\nimport { Label } from \"@/components/ui/label\"\nimport { cn } from \"@/lib/utils\"\nimport type { UnlockPlatform, UnlockWalletResult } from \"./use-unlock-wallet\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport interface UnlockWalletUiProps {\n  /** Platform determines which unlock methods are shown */\n  platform: UnlockPlatform\n  /** Application name displayed in the UI */\n  appName: string\n  /** Whether an unlock is in progress */\n  isLoading: boolean\n  /** Current error state */\n  error: Error | null\n  /** Whether the wallet is already unlocked */\n  isUnlocked: boolean\n  /** Number of failed attempts */\n  failedAttempts: number\n  /** Called when the user attempts to unlock */\n  onSubmit: (passphrase?: string) => Promise<UnlockWalletResult>\n  /** Reset error state */\n  onReset: () => void\n  /** Optional CSS class */\n  className?: string\n}\n\n// ---------------------------------------------------------------------------\n// Component\n// ---------------------------------------------------------------------------\n\nexport function UnlockWalletUi({\n  platform,\n  appName,\n  isLoading,\n  error,\n  isUnlocked,\n  failedAttempts,\n  onSubmit,\n  onReset,\n  className,\n}: UnlockWalletUiProps) {\n  const [passphrase, setPassphrase] = useState(\"\")\n  const [showPassphrase, setShowPassphrase] = useState(false)\n\n  const isMac = platform === \"macos\"\n\n  const handleBiometricUnlock = useCallback(async () => {\n    await onSubmit(undefined)\n  }, [onSubmit])\n\n  const handlePassphraseUnlock = useCallback(async () => {\n    if (!passphrase.trim()) return\n    await onSubmit(passphrase)\n    setPassphrase(\"\")\n  }, [onSubmit, passphrase])\n\n  const handleKeyDown = useCallback(\n    (e: React.KeyboardEvent<HTMLInputElement>) => {\n      if (e.key === \"Enter\") {\n        e.preventDefault()\n        void handlePassphraseUnlock()\n      }\n    },\n    [handlePassphraseUnlock],\n  )\n\n  const handleShowPassphrase = useCallback(() => {\n    setShowPassphrase(true)\n    onReset()\n  }, [onReset])\n\n  // Unlocked state\n  if (isUnlocked) {\n    return (\n      <Card className={cn(\"w-full max-w-sm\", className)}>\n        <CardContent className=\"flex flex-col items-center gap-4 pt-6\">\n          <div className=\"flex size-16 items-center justify-center rounded-full bg-primary/10\">\n            <ShieldCheck className=\"size-8 text-primary\" aria-hidden=\"true\" />\n          </div>\n          <div className=\"flex flex-col items-center gap-1 text-center\">\n            <p className=\"text-lg font-semibold\">Wallet Unlocked</p>\n            <p className=\"text-sm text-muted-foreground\">\n              {appName} is ready to use\n            </p>\n          </div>\n          <Badge variant=\"outline\" className=\"gap-1.5\">\n            <CheckCircle2 className=\"size-3\" aria-hidden=\"true\" />\n            Authenticated\n          </Badge>\n        </CardContent>\n      </Card>\n    )\n  }\n\n  // macOS biometric-first layout\n  if (isMac && !showPassphrase) {\n    return (\n      <Card className={cn(\"w-full max-w-sm\", className)}>\n        <CardHeader className=\"items-center text-center\">\n          <div className=\"mb-2 flex size-16 items-center justify-center rounded-full bg-muted\">\n            <Lock className=\"size-8 text-muted-foreground\" aria-hidden=\"true\" />\n          </div>\n          <CardTitle>{appName}</CardTitle>\n          <CardDescription>\n            Use Touch ID to unlock your wallet\n          </CardDescription>\n        </CardHeader>\n\n        <CardContent className=\"flex flex-col items-center gap-4\">\n          <Button\n            size=\"lg\"\n            className=\"w-full gap-2\"\n            onClick={() => { void handleBiometricUnlock() }}\n            disabled={isLoading}\n            aria-busy={isLoading}\n          >\n            {isLoading ? (\n              <>\n                <Loader2\n                  className=\"animate-spin\"\n                  data-icon=\"inline-start\"\n                  aria-hidden=\"true\"\n                />\n                Authenticating...\n              </>\n            ) : (\n              <>\n                <Fingerprint data-icon=\"inline-start\" aria-hidden=\"true\" />\n                Unlock with Touch ID\n              </>\n            )}\n          </Button>\n\n          {/* Error */}\n          {error && (\n            <div className=\"flex w-full 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\">Unlock failed</p>\n                <p className=\"text-xs text-muted-foreground\">\n                  {error.message}\n                </p>\n              </div>\n            </div>\n          )}\n\n          {failedAttempts > 0 && (\n            <Badge variant=\"secondary\" className=\"text-xs\">\n              {failedAttempts} failed {failedAttempts === 1 ? \"attempt\" : \"attempts\"}\n            </Badge>\n          )}\n        </CardContent>\n\n        <CardFooter className=\"justify-center\">\n          <Button\n            variant=\"link\"\n            size=\"sm\"\n            className=\"text-xs text-muted-foreground\"\n            onClick={handleShowPassphrase}\n          >\n            Use passphrase instead\n          </Button>\n        </CardFooter>\n      </Card>\n    )\n  }\n\n  // Passphrase layout (default for \"other\" or fallback from macOS)\n  return (\n    <Card className={cn(\"w-full max-w-sm\", className)}>\n      <CardHeader className=\"items-center text-center\">\n        <div className=\"mb-2 flex size-16 items-center justify-center rounded-full bg-muted\">\n          <Lock className=\"size-8 text-muted-foreground\" aria-hidden=\"true\" />\n        </div>\n        <CardTitle>{appName}</CardTitle>\n        <CardDescription>\n          Enter your passphrase to unlock your wallet\n        </CardDescription>\n      </CardHeader>\n\n      <CardContent className=\"flex flex-col gap-4\">\n        <div className=\"flex flex-col gap-2\">\n          <Label htmlFor=\"unlock-passphrase\">Passphrase</Label>\n          <Input\n            id=\"unlock-passphrase\"\n            type=\"password\"\n            placeholder=\"Enter your passphrase\"\n            value={passphrase}\n            onChange={(e) => setPassphrase(e.target.value)}\n            onKeyDown={handleKeyDown}\n            disabled={isLoading}\n            autoComplete=\"current-password\"\n            autoFocus\n          />\n        </div>\n\n        {/* Error */}\n        {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\">Unlock failed</p>\n              <p className=\"text-xs text-muted-foreground\">{error.message}</p>\n            </div>\n          </div>\n        )}\n\n        {failedAttempts > 0 && (\n          <div className=\"flex justify-center\">\n            <Badge variant=\"secondary\" className=\"text-xs\">\n              {failedAttempts} failed {failedAttempts === 1 ? \"attempt\" : \"attempts\"}\n            </Badge>\n          </div>\n        )}\n      </CardContent>\n\n      <CardFooter className=\"flex flex-col gap-2\">\n        <Button\n          className=\"w-full gap-2\"\n          onClick={() => { void handlePassphraseUnlock() }}\n          disabled={isLoading || !passphrase.trim()}\n          aria-busy={isLoading}\n        >\n          {isLoading ? (\n            <>\n              <Loader2\n                className=\"animate-spin\"\n                data-icon=\"inline-start\"\n                aria-hidden=\"true\"\n              />\n              Unlocking...\n            </>\n          ) : (\n            <>\n              <KeyRound data-icon=\"inline-start\" aria-hidden=\"true\" />\n              Unlock Wallet\n            </>\n          )}\n        </Button>\n\n        {isMac && (\n          <Button\n            variant=\"link\"\n            size=\"sm\"\n            className=\"text-xs text-muted-foreground\"\n            onClick={() => {\n              setShowPassphrase(false)\n              onReset()\n            }}\n          >\n            Use Touch ID instead\n          </Button>\n        )}\n      </CardFooter>\n    </Card>\n  )\n}\n",
      "type": "registry:component",
      "target": "~/components/blocks/unlock-wallet/unlock-wallet-ui.tsx"
    },
    {
      "path": "registry/new-york/blocks/unlock-wallet/use-unlock-wallet.ts",
      "content": "import { useCallback, useState } from \"react\"\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** Supported platform values */\nexport type UnlockPlatform = \"macos\" | \"other\"\n\n/** Result returned from the onUnlock callback */\nexport interface UnlockWalletResult {\n  /** Whether the unlock was successful */\n  success: boolean\n  /** Error message on failure */\n  error?: string\n}\n\n/** Options for the useUnlockWallet hook */\nexport interface UseUnlockWalletOptions {\n  /** Platform determines which unlock methods are available */\n  platform?: UnlockPlatform\n  /** Application name displayed in the unlock UI */\n  appName?: string\n  /** Callback to execute the actual unlock */\n  onUnlock?: (passphrase?: string) => Promise<UnlockWalletResult>\n  /** Called on successful unlock */\n  onSuccess?: () => void\n  /** Called on error */\n  onError?: (error: Error) => void\n}\n\n/** Return type of useUnlockWallet hook */\nexport interface UseUnlockWalletReturn {\n  /** Whether an unlock operation is in progress */\n  isLoading: boolean\n  /** Current error, if any */\n  error: Error | null\n  /** Whether the wallet has been unlocked */\n  isUnlocked: boolean\n  /** Number of failed attempts */\n  failedAttempts: number\n  /** Execute an unlock attempt */\n  execute: (passphrase?: string) => Promise<UnlockWalletResult>\n  /** Reset error state */\n  reset: () => void\n}\n\n// ---------------------------------------------------------------------------\n// Hook\n// ---------------------------------------------------------------------------\n\nexport function useUnlockWallet({\n  onUnlock,\n  onSuccess,\n  onError,\n}: UseUnlockWalletOptions = {}): UseUnlockWalletReturn {\n  const [isLoading, setIsLoading] = useState(false)\n  const [error, setError] = useState<Error | null>(null)\n  const [isUnlocked, setIsUnlocked] = useState(false)\n  const [failedAttempts, setFailedAttempts] = useState(0)\n\n  const execute = useCallback(\n    async (passphrase?: string): Promise<UnlockWalletResult> => {\n      if (!onUnlock) {\n        const err = new Error(\"onUnlock callback is required\")\n        setError(err)\n        onError?.(err)\n        return { success: false, error: err.message }\n      }\n\n      setIsLoading(true)\n      setError(null)\n\n      try {\n        const result = await onUnlock(passphrase)\n\n        if (!result.success) {\n          const errMsg = result.error ?? \"Unlock failed\"\n          const err = new Error(errMsg)\n          setError(err)\n          setFailedAttempts((prev) => prev + 1)\n          onError?.(err)\n          return result\n        }\n\n        setIsUnlocked(true)\n        setFailedAttempts(0)\n        onSuccess?.()\n        return result\n      } catch (err) {\n        const e = err instanceof Error ? err : new Error(String(err))\n        setError(e)\n        setFailedAttempts((prev) => prev + 1)\n        onError?.(e)\n        return { success: false, error: e.message }\n      } finally {\n        setIsLoading(false)\n      }\n    },\n    [onUnlock, onSuccess, onError],\n  )\n\n  const reset = useCallback(() => {\n    setError(null)\n  }, [])\n\n  return { isLoading, error, isUnlocked, failedAttempts, execute, reset }\n}\n",
      "type": "registry:component",
      "target": "~/components/blocks/unlock-wallet/use-unlock-wallet.ts"
    }
  ],
  "type": "registry:block"
}