# Passphrase Gate

Passphrase Gate [#passphrase-gate]

The **Passphrase Gate** is a controlled, presentational unlock/create screen for
a passphrase-protected surface. **Create** mode renders passphrase + confirmation
fields, enforces a minimum length, and shows strength by composing the
[Password Strength Bar](/docs/security-privacy/password-strength-bar) from
caller-computed props. **Unlock** mode renders a single field with a
caller-provided error surface (e.g. "Incorrect passphrase"). Submission emits the
passphrase via `onSubmit`.

> **Backend-agnostic.** The gate performs no crypto, no storage access, and holds
> no passphrase beyond its controlled field state — the app derives the key and
> drives the vault. Works with any backend; the recommended LocalMode hook is
> `useEncryptedVault` from `@localmode/react` (`unlock(passphrase)`, `status`,
> `error`) — a recommendation, never a requirement.

**When to use it:** any vault, encrypted-store, or key-derivation form that needs
a create-vs-unlock passphrase screen with strength feedback and error handling.

Preview [#preview]

```tsx
'use client';

import { useState } from 'react';

import { PassphraseGate, type PassphraseStrength } from '@/components/passphrase-gate';

/** Demo-only strength estimator — length + character-class heuristic (0–100). */
function estimate(value: string): PassphraseStrength {
  const classes =
    (/[a-z]/.test(value) ? 1 : 0) +
    (/[A-Z]/.test(value) ? 1 : 0) +
    (/[0-9]/.test(value) ? 1 : 0) +
    (/[^a-zA-Z0-9]/.test(value) ? 1 : 0);
  const lengthScore = Math.min(value.length, 16) / 16;
  const score = Math.round(lengthScore * 60 + (classes / 4) * 40);
  return {
    value: score,
    label: value.length === 0 ? undefined : score < 40 ? 'Weak' : score < 70 ? 'Good' : 'Strong',
    color: score < 40 ? 'error' : score < 70 ? 'warning' : 'success',
  };
}

/**
 * Demo for PassphraseGate, used by the docs live preview. Fixture-driven — a
 * toggle flips between create and unlock modes; the app-side strength estimator
 * feeds the composed strength bar. No crypto, no storage, no model: submitting
 * just records the last mode. In production, wire `onSubmit` to
 * `useEncryptedVault().unlock`.
 */
export default function PassphraseGateDemo() {
  const [mode, setMode] = useState<'create' | 'unlock'>('create');
  const [strength, setStrength] = useState<PassphraseStrength>({ value: 0 });
  const [submitted, setSubmitted] = useState<string | null>(null);
  // Show a fake rejection in unlock mode to demonstrate the error surface.
  const [error, setError] = useState<string | undefined>(undefined);

  return (
    <div className="flex w-full max-w-sm flex-col gap-3">
      <div className="flex items-center gap-2 text-xs">
        <button
          type="button"
          onClick={() => {
            setMode('create');
            setError(undefined);
            setSubmitted(null);
          }}
          data-active={mode === 'create'}
          className="rounded-md border border-border px-2.5 py-1 font-medium data-[active=true]:bg-accent"
        >
          Create
        </button>
        <button
          type="button"
          onClick={() => {
            setMode('unlock');
            setError(undefined);
            setSubmitted(null);
          }}
          data-active={mode === 'unlock'}
          className="rounded-md border border-border px-2.5 py-1 font-medium data-[active=true]:bg-accent"
        >
          Unlock
        </button>
      </div>

      <PassphraseGate
        mode={mode}
        strength={strength}
        error={error}
        onPassphraseChange={(v) => setStrength(estimate(v))}
        onSubmit={(p) => {
          if (mode === 'unlock' && p !== 'correct horse') {
            setError('Incorrect passphrase');
            return;
          }
          setError(undefined);
          setSubmitted(p ? `${p.length}-char passphrase accepted` : null);
        }}
      />

      {submitted && <p className="text-xs text-emerald-600 dark:text-emerald-400">{submitted}</p>}
    </div>
  );
}
```

Installation [#installation]

```bash
npx shadcn@latest add @localmode/ui/security-privacy/passphrase-gate
```

Dependencies [#dependencies]

* **Data source:** controlled and presentational — emits `onSubmit(passphrase)` and `onPassphraseChange(value)`; it never estimates strength or touches crypto. Recommended LocalMode hook: `useEncryptedVault` (optional).
* `lucide-react` — icons
* `clsx` + `tailwind-merge` — via the shared `cn()` util (installed automatically as a registry dependency)
* Composes `@localmode/ui/security-privacy/password-strength-bar` (installed automatically as a registry dependency)

Files installed [#files-installed]

* `passphrase-gate.tsx` — the component
* `password-strength-bar.tsx` — the composed strength meter
* `lib/utils.ts` — the `cn()` helper (if not already present)

Props [#props]

**PassphraseGate**

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `mode` | `"unlock" \| "create"` | — | **Required.** `'create'` renders passphrase + confirmation fields with a strength meter; `'unlock'` renders a single passphrase field with an error surface. |
| `onSubmit` | `function` | — | **Required.** Fired with the entered passphrase when the (validated) form is submitted. The gate performs no crypto and retains no passphrase beyond its controlled field state. |
| `onPassphraseChange` | `function` | — | Fired on every keystroke in the primary passphrase field, so the app can compute and feed back `strength` (create mode) without the gate estimating anything. |
| `strength` | `object` | — | Caller-computed strength for the composed strength bar (create mode only). Omit to hide the bar. |
| `minLength` | `number` | `8` | Minimum passphrase length enforced in create mode (with mismatch/too-short feedback). |
| `error` | `string` | — | A caller-provided error to surface (e.g. "Incorrect passphrase" after a failed unlock). Re-enables input. |
| `isBusy` | `boolean` | — | When true, disables inputs and shows a busy spinner on submit. |
| `title` | `ReactNode` | — | Title slot (heading above the fields). |
| `description` | `ReactNode` | — | Description slot (sub-text under the title). |
| `submitLabel` | `string` | — | Submit-button label override. Defaults per mode. |

**PassphraseStrength**

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `number` | — | **Required.** Strength score, 0–100. |
| `label` | `string` | — | Human-readable label (e.g. "Weak", "Good", "Strong"). |
| `color` | `"success" \| "warning" \| "error"` | — | Semantic color token. |

Examples [#examples]

Wiring to an encrypted vault [#wiring-to-an-encrypted-vault]

The app owns the estimator and the vault; the gate only renders and reports intent:

```tsx
import { useEncryptedVault } from '@localmode/react';
import { PassphraseGate } from '@/components/passphrase-gate';

function estimate(value: string) {
  const classes =
    (/[a-z]/.test(value) ? 1 : 0) + (/[A-Z]/.test(value) ? 1 : 0) +
    (/[0-9]/.test(value) ? 1 : 0) + (/[^a-zA-Z0-9]/.test(value) ? 1 : 0);
  const score = Math.round((Math.min(value.length, 16) / 16) * 60 + (classes / 4) * 40);
  return { value: score, label: score < 40 ? 'Weak' : score < 70 ? 'Good' : 'Strong',
    color: score < 40 ? 'error' : score < 70 ? 'warning' : 'success' } as const;
}

export function VaultGate() {
  const { status, error, unlock } = useEncryptedVault({ name: 'notes' });
  const [strength, setStrength] = useState({ value: 0 });
  return (
    <PassphraseGate
      mode={status === 'uninitialized' ? 'create' : 'unlock'}
      onSubmit={unlock}
      onPassphraseChange={(v) => setStrength(estimate(v))}
      strength={strength}
      error={error?.name === 'VaultPassphraseError' ? 'Incorrect passphrase' : undefined}
    />
  );
}
```

Unlock mode with a busy state [#unlock-mode-with-a-busy-state]

```tsx
<PassphraseGate mode="unlock" isBusy={isDeriving} onSubmit={unlock} error={lastError} />
```

Customization [#customization]

Create mode gates submission until the passphrase reaches `minLength` (default 8)
and the confirmation matches; the strength bar renders only when you pass
`strength`. Unlock mode shows a single field plus the `error` surface. Because you
own the copied file, every threshold, label, and class is yours to change — swap
the composed strength bar, add a "show passphrase" toggle, or wire your own
key-derivation flow.