/**
 * The Vault browser-extension popup, as a shared component.
 *
 * A faithful recreation of `clients/extension` — the real popup's routes,
 * layout, copy, field labels, sort options and tag/folder structure — running on
 * sample data that never leaves the device. Interactive: open an item, copy a
 * field, reveal a TOTP, switch tabs, walk into settings.
 *
 * It lives HERE rather than in either consumer because both the marketing site
 * and the Vault app show it, and two copies of a 1,000-line recreation would
 * drift the moment the real extension changed. Same reasoning as sharing
 * `PasswordGenerator` instead of rewriting it per app.
 *
 * Two things the website's original did that this deliberately does not:
 *
 *  * **No `auto` mode.** That drove a synthetic cursor with `querySelector` and
 *    `getBoundingClientRect`, which has no meaning on native. It belongs to that
 *    page's choreography, not to the extension, so the showcase keeps it.
 *  * **No drag-and-drop in the tags tab.** It used `@dnd-kit`, which is DOM-only.
 *    The grip handles remain as affordances because removing them would change
 *    the layout this exists to reproduce, but they do not drag.
 *
 * Everything else is the original, moved rather than rewritten.
 */
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { Image, Linking, Pressable, ScrollView, Text as RNText, TextInput, View, } from "react-native";
import { useColorScheme, vars } from "nativewind";
import { ArrowDownUp, ArrowLeft, Check, ChevronDown, ChevronLeft, ChevronRight, ChevronUp, Circle as CircleIcon, Copy, ExternalLink, Eye, EyeOff, Fingerprint, Folder, FolderPlus, GripVertical, KeyRound, Lock, LogOut, Pencil, Plus, Search, Send, Server, Settings as SettingsIcon, ShieldAlert, SquarePen, Star, Tag as TagIcon, Trash2, } from "lucide-react-native";
import { MutedText, Text } from "../components/text";
import { Button } from "../components/button";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from "../components/card";
/**
 * The real popup's frame, in px.
 *
 * `HEIGHT` is Chrome's *maximum* for a browser action popup, not a fixed size —
 * the real popup is as tall as it needs to be, up to this. So a host with less
 * vertical room may render it shorter (see the `height` prop) without the thing
 * on screen becoming a lie about what the extension looks like; the item list
 * scrolls inside, exactly as it does at full height.
 */
export const EXTENSION_POPUP_WIDTH = 380;
export const EXTENSION_POPUP_HEIGHT = 600;
/** Below this the chrome crowds out the list and it stops reading as the popup. */
export const EXTENSION_POPUP_MIN_HEIGHT = 320;
export function useInk() {
    const { colorScheme: scheme } = useColorScheme();
    const dark = scheme === "dark";
    // Vault's own palette (pure white in light), matching .vault-surface.
    return {
        fg: dark ? "#ffffff" : "#000000",
        muted: dark ? "#a3a3a3" : "#737373",
        bg: dark ? "#000000" : "#ffffff",
        border: dark ? "#262626" : "#e5e5e5",
        secondary: dark ? "#262626" : "#f5f5f5",
        danger: "#ef4444",
        star: "#f59e0b",
    };
}
// Demo graphics are react-native-web, which SSRs markup that then re-lays-out
// on hydration ("pops in") and would render with the pre-mount theme guess
// (a dark flash on a light page). Gate them to mount client-side, behind a
// theme-correct placeholder, so they appear once, already in the right theme.
export function vaultVars(dark) {
    return vars(dark
        ? {
            "--background-rgb": "0 0 0",
            "--foreground-rgb": "255 255 255",
            "--card": "#0a0a0a",
            "--card-foreground": "#ffffff",
            "--secondary": "#262626",
            "--secondary-foreground": "#ffffff",
            "--muted": "#262626",
            "--muted-foreground": "#a3a3a3",
            "--border": "#262626",
            "--primary": "#ffffff",
            "--primary-foreground": "#000000",
        }
        : {
            "--background-rgb": "255 255 255",
            "--foreground-rgb": "0 0 0",
            "--card": "#ffffff",
            "--card-foreground": "#000000",
            "--secondary": "#f5f5f5",
            "--secondary-foreground": "#000000",
            "--muted": "#f5f5f5",
            "--muted-foreground": "#737373",
            "--border": "#e5e5e5",
            "--primary": "#000000",
            "--primary-foreground": "#ffffff",
        });
}
export const T = {
    social: { name: "social", color: "#3b82f6", icon: "circle" },
    shopping: { name: "shopping", color: "#22c55e", icon: "tag" },
    finance: { name: "finance", color: "#f59e0b", icon: "star" },
    streaming: { name: "streaming", color: "#a855f7", icon: "circle" },
    gaming: { name: "gaming", color: "#06b6d4", icon: "tag" },
};
export const ITEMS = [
    {
        id: "1",
        name: "Netflix",
        url: "https://netflix.com",
        favorite: true,
        tags: [T.streaming],
        created: "2024-02-11",
        updated: "2025-05-30",
        accounts: [
            {
                username: "example@entropysoftworks.com",
                password: "tR0ub4dour&3xplsion",
                totp_secret: "JBSWY3DPEHPK3PXP",
                notes: "Shared family plan.",
            },
        ],
    },
    {
        id: "2",
        name: "Amazon",
        url: "https://amazon.com",
        favorite: true,
        tags: [T.shopping],
        created: "2024-01-04",
        updated: "2025-05-22",
        accounts: [
            {
                username: "example@entropysoftworks.com",
                password: "correct-horse-battery",
                totp_secret: "KRSXG5CTMVRXEZLU",
            },
        ],
    },
    {
        id: "3",
        name: "Instagram",
        url: "https://instagram.com",
        favorite: true,
        tags: [T.social],
        created: "2024-03-02",
        updated: "2025-06-01",
        accounts: [
            { username: "yourhandle", password: "9xQvM2pL7wE4z", totp_secret: "MFRGGZDFMZTWQ2LK" },
        ],
    },
    {
        id: "4",
        name: "PayPal",
        url: "https://paypal.com",
        favorite: false,
        tags: [T.finance],
        created: "2024-04-20",
        updated: "2025-05-12",
        accounts: [
            {
                username: "example@entropysoftworks.com",
                password: "Zx8kLp2qWn5vR",
                totp_secret: "GEZDGNBVGY3TQOJQ",
            },
        ],
    },
    {
        id: "5",
        name: "Chase",
        url: "https://chase.com",
        favorite: false,
        tags: [T.finance],
        created: "2024-02-18",
        updated: "2025-04-28",
        accounts: [
            {
                username: "example@entropysoftworks.com",
                password: "Sf6!nQ9xK3mD2p",
                notes: "Checking + card.",
            },
        ],
    },
    {
        id: "6",
        name: "Spotify",
        url: "https://spotify.com",
        favorite: false,
        tags: [T.streaming],
        created: "2024-05-30",
        updated: "2025-03-19",
        accounts: [{ username: "example@entropysoftworks.com", password: "Vb7nQ9xK3mD2p" }],
    },
    {
        id: "7",
        name: "Steam",
        url: "https://steampowered.com",
        favorite: false,
        tags: [T.gaming],
        created: "2024-06-01",
        updated: "2025-02-09",
        accounts: [{ username: "yourhandle", password: "Lr4!tWp2qZ", totp_secret: "NB2W45DFOIZA" }],
    },
    {
        id: "8",
        name: "Reddit",
        url: "https://reddit.com",
        favorite: false,
        tags: [T.social],
        created: "2024-01-22",
        updated: "2025-01-15",
        accounts: [{ username: "u/yourhandle", password: "Dx2!mNp9qR" }],
    },
];
export const SORT_OPTIONS = [
    {
        value: "name_asc",
        label: "Name (A-Z)",
        cmp: (a, b) => a.name.localeCompare(b.name),
    },
    {
        value: "name_desc",
        label: "Name (Z-A)",
        cmp: (a, b) => b.name.localeCompare(a.name),
    },
    {
        value: "updated_newest",
        label: "Recently updated",
        cmp: (a, b) => b.updated.localeCompare(a.updated),
    },
    {
        value: "created_newest",
        label: "Newest first",
        cmp: (a, b) => b.created.localeCompare(a.created),
    },
    {
        value: "created_oldest",
        label: "Oldest first",
        cmp: (a, b) => a.created.localeCompare(b.created),
    },
];
/** The sort used when an index somehow falls outside SORT_OPTIONS. */
const DEFAULT_SORT = SORT_OPTIONS[0];
// ── helpers ───────────────────────────────────────────────────────────────
export function host(url) {
    return url.replace(/^https?:\/\//, "").replace(/\/.*$/, "");
}
// Real ItemIcon renders the site favicon; reproduce with the same favicon
// service the app's icon resolver falls back to.
export function favicon(url) {
    return `https://www.google.com/s2/favicons?domain=${host(url)}&sz=64`;
}
export function ItemIcon({ url, size = 40, radius = 8, }) {
    return (<View style={{
            width: size,
            height: size,
            borderRadius: radius,
            overflow: "hidden",
            alignItems: "center",
            justifyContent: "center",
            backgroundColor: "#ffffff",
            borderWidth: 1,
            borderColor: "#e5e5e5",
        }}>
      {/* RN `Image`, not a DOM `<img>`: this component renders on native too,
            where `<img>` is not a thing. `resizeMode="contain"` is the RN
            spelling of `object-fit: contain`. */}
      <Image source={{ uri: favicon(url) }} accessibilityIgnoresInvertColors style={{ width: size * 0.6, height: size * 0.6 }} resizeMode="contain"/>
    </View>);
}
export function useCopy() {
    const [copied, setCopied] = useState(null);
    const copy = useCallback((key, text) => {
        if (typeof navigator !== "undefined" && navigator.clipboard) {
            navigator.clipboard.writeText(text).catch(() => { });
        }
        setCopied(key);
        setTimeout(() => setCopied((k) => (k === key ? null : k)), 1300);
    }, []);
    return { copied, copy };
}
// Deterministic 6-digit code that rolls every 30s (stand-in for real TOTP).
export function totpCode(seed, step) {
    let h = 0;
    for (const c of seed) {
        h = (h * 31 + c.charCodeAt(0)) >>> 0;
    }
    return Math.abs((h ^ (step * 2654435761)) % 1_000_000)
        .toString()
        .padStart(6, "0");
}
export function useTotp(secret) {
    const [now, setNow] = useState(0);
    useEffect(() => {
        const t = setInterval(() => setNow(Date.now()), 1000);
        setNow(Date.now());
        return () => clearInterval(t);
    }, []);
    if (!secret || !now) {
        return { code: "------", remaining: 30 };
    }
    const epoch = Math.floor(now / 1000);
    return { code: totpCode(secret, Math.floor(epoch / 30)), remaining: 30 - (epoch % 30) };
}
// Matches the app's RingCountdown widget.
export function RingCountdown({ remaining, size = 48, period = 30, color, }) {
    const r = size / 2 - 3;
    const c = 2 * Math.PI * r;
    const frac = remaining / period;
    const warn = remaining <= 5;
    const stroke = warn ? "#ef4444" : color;
    return (<View style={{ width: size, height: size }}>
      <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
        <circle cx={size / 2} cy={size / 2} r={r} fill="none" stroke={color} strokeOpacity={0.2} strokeWidth={3}/>
        <circle cx={size / 2} cy={size / 2} r={r} fill="none" stroke={stroke} strokeWidth={3} strokeLinecap="round" strokeDasharray={c} strokeDashoffset={c * (1 - frac)} transform={`rotate(-90 ${size / 2} ${size / 2})`}/>
        <text x={size / 2} y={size / 2 + 4} textAnchor="middle" fontSize={12} fontFamily="var(--font-geist-mono)" fill={stroke}>
          {remaining}
        </text>
      </svg>
    </View>);
}
// ── phone frame ─────────────────────────────────────────────────────────────
export function VaultItemRow({ item, ink, onPress, highlight, }) {
    // Sample data always has one; the guard is for the type checker, and
    // rendering nothing beats crashing if that ever stops being true.
    const acc = item.accounts[0];
    return (<Pressable nativeID={`vrow-${item.id}`} onPress={onPress} className="mb-1.5 flex-row items-center overflow-hidden rounded-lg bg-card p-3.5 active:opacity-80" style={highlight ? { backgroundColor: ink.secondary } : undefined}>
      <View className="mr-3">
        <ItemIcon url={item.url}/>
      </View>
      <View className="flex-1">
        <Text className="text-sm font-semibold" numberOfLines={1}>
          {item.name}
        </Text>
        <MutedText className="text-xs" numberOfLines={1}>
          {acc?.username || host(item.url)}
        </MutedText>
      </View>
      {item.favorite && <Star size={14} color={ink.fg} fill={ink.fg}/>}
      <Pressable className="ml-1 px-1.5 py-1 active:opacity-70" onPress={() => void Linking.openURL(item.url).catch(() => undefined)}>
        <ExternalLink size={18} color={ink.muted}/>
      </Pressable>
      <ChevronRight size={18} color={ink.muted}/>
    </Pressable>);
}
export function Dropdown({ open, onToggle, label, icon, children, ink, }) {
    return (<View style={{ position: "relative" }}>
      <Pressable onPress={onToggle} className="flex-row items-center gap-1 rounded-md border border-border px-2 py-1 active:opacity-70">
        {icon}
        <MutedText className="text-xs">{label}</MutedText>
        {open ? (<ChevronUp size={10} color={ink.muted}/>) : (<ChevronDown size={10} color={ink.muted}/>)}
      </Pressable>
      {open && (<View className="absolute right-0 top-8 z-50 rounded-lg border border-border bg-card py-1" style={{ minWidth: 160 }}>
          {children}
        </View>)}
    </View>);
}
export function VaultList({ ink, onOpen, items = ITEMS, scrollRef, highlightId, }) {
    const [query, setQuery] = useState("");
    const [sortIdx, setSortIdx] = useState(0);
    const [showSort, setShowSort] = useState(false);
    // `?? SORT_OPTIONS[0]!` rather than a non-null assertion on the index:
    // sortIdx is driven by the picker below and cannot leave range, but under
    // noUncheckedIndexedAccess the compiler cannot know that, and falling back to
    // the default sort is the right behaviour if it ever did.
    // Falls back to the first option rather than asserting non-null: sortIdx is
    // driven by the picker and cannot leave range, but the default sort is the
    // right behaviour if it ever did.
    const sort = SORT_OPTIONS[sortIdx] ?? DEFAULT_SORT;
    const filtered = useMemo(() => {
        const q = query.trim().toLowerCase();
        return items.filter((i) => !q ||
            i.name.toLowerCase().includes(q) ||
            i.accounts.some((a) => a.username.toLowerCase().includes(q)) ||
            host(i.url).includes(q));
    }, [query, items]);
    const favorites = filtered.filter((i) => i.favorite);
    const all = [...filtered.filter((i) => !i.favorite)].sort(sort.cmp);
    return (<View className="flex-1">
      <View className="px-4 pb-2 pt-1">
        <View className="flex-row items-center rounded-lg border border-border px-3" style={{ height: 40 }}>
          <Search size={18} color={ink.muted}/>
          <TextInput value={query} onChangeText={setQuery} placeholder="Search passwords" placeholderTextColor={ink.muted} className="ml-2 flex-1 text-sm text-foreground" style={{ outlineStyle: "none" }}/>
        </View>
      </View>

      <ScrollView ref={scrollRef} className="flex-1" contentContainerStyle={{ paddingHorizontal: 16, paddingBottom: 20 }}>
        {favorites.length > 0 && (<View className="pt-2">
            <View className="mb-2 flex-row items-center gap-1.5 px-1">
              <Star size={14} color={ink.fg}/>
              <Text className="text-xs font-semibold uppercase tracking-wider">Favorites</Text>
              <MutedText className="text-xs">({favorites.length})</MutedText>
            </View>
            {favorites.map((i) => (<VaultItemRow key={i.id} item={i} ink={ink} onPress={() => onOpen(i.id)} highlight={highlightId === i.id}/>))}
            <View className="my-3 h-px bg-border"/>
          </View>)}

        {all.length > 0 && (<View style={{ zIndex: 50 }}>
            <View className="relative mb-2 flex-row items-center justify-between px-1" style={{ zIndex: 50 }}>
              <View className="flex-row items-center gap-1.5">
                <Text className="text-xs font-semibold uppercase tracking-wider">All Items</Text>
                <MutedText className="text-xs">({all.length})</MutedText>
              </View>
              <Dropdown ink={ink} open={showSort} onToggle={() => setShowSort((v) => !v)} label={sort.label} icon={<ArrowDownUp size={12} color={ink.muted}/>}>
                {SORT_OPTIONS.map((o, idx) => (<Pressable key={o.value} onPress={() => {
                    setSortIdx(idx);
                    setShowSort(false);
                }} className={`px-3 py-1.5 active:opacity-70 ${idx === sortIdx ? "bg-secondary" : ""}`}>
                    <Text className={`text-xs ${idx === sortIdx ? "font-semibold" : ""}`}>
                      {o.label}
                    </Text>
                  </Pressable>))}
              </Dropdown>
            </View>
            {all.map((i) => (<VaultItemRow key={i.id} item={i} ink={ink} onPress={() => onOpen(i.id)} highlight={highlightId === i.id}/>))}
          </View>)}
      </ScrollView>
    </View>);
}
// ── item detail (app/(tabs)/vault/[id]/index.tsx) ────────────────────────────
export function FieldRow({ label, value, ink, copied, copy, secret, totp, }) {
    const [show, setShow] = useState(false);
    const isCopied = copied === label;
    const display = totp ? totp.code : secret && !show ? "••••••••••••" : value;
    return (<View className="mb-4">
      <Text className="mb-1 text-xs text-muted-foreground">{label}</Text>
      <View className="flex-row items-center justify-between rounded-lg bg-secondary p-3">
        <RNText className="flex-1 font-mono text-sm text-foreground" numberOfLines={1} style={{ color: ink.fg }}>
          {display}
        </RNText>
        <View className="flex-row items-center gap-2">
          {secret && (<Pressable onPress={() => setShow((v) => !v)} className="active:opacity-60">
              {show ? <EyeOff size={18} color={ink.muted}/> : <Eye size={18} color={ink.muted}/>}
            </Pressable>)}
          {totp && (<View className="mr-1">
              <RingCountdown remaining={totp.remaining} color={ink.fg}/>
            </View>)}
          <Pressable nativeID={totp ? "fcopy-totp" : undefined} onPress={() => copy(label, totp ? totp.code : value)} className="active:opacity-60">
            {isCopied ? <Check size={18} color="#22c55e"/> : <Copy size={18} color={ink.muted}/>}
          </Pressable>
        </View>
      </View>
    </View>);
}
export function ItemDetail({ item, ink, onBack, onDelete, forceCopiedLabel, scrollRef, }) {
    // Sample data always has one; the guard is for the type checker, and
    // rendering nothing beats crashing if that ever stops being true.
    const acc = item.accounts[0];
    const accSecret = acc?.totp_secret;
    const { copied, copy } = useCopy();
    const totp = useTotp(accSecret);
    const [editing, setEditing] = useState(false);
    const [confirm, setConfirm] = useState(false);
    // In autoplay the copied highlight is driven externally; otherwise the
    // real per-field copy state.
    const effCopied = forceCopiedLabel ?? copied;
    if (editing) {
        return <ExtItemForm ink={ink} item={item} onCancel={() => setEditing(false)}/>;
    }
    // After the hooks, so the hook order never changes. Sample items always carry
    // an account; this is the type checker's guard, and rendering nothing beats
    // crashing if that ever stops being true.
    if (!acc) {
        return null;
    }
    function launch() {
        void Linking.openURL(item.url).catch(() => undefined);
    }
    return (<ScrollView ref={scrollRef} className="flex-1" contentContainerStyle={{ padding: 16 }}>
      {/* top bar */}
      <View className="mb-4 flex-row items-center justify-between">
        <Pressable onPress={onBack} className="active:opacity-70">
          <ArrowLeft size={22} color={ink.fg}/>
        </Pressable>
        <View className="flex-row gap-4">
          <Pressable onPress={launch} hitSlop={8} className="active:opacity-60">
            <ExternalLink size={20} color={ink.fg}/>
          </Pressable>
          <Pressable onPress={() => setEditing(true)} hitSlop={8} className="active:opacity-60">
            <SquarePen size={20} color={ink.fg}/>
          </Pressable>
          <Pressable onPress={() => setConfirm(true)} hitSlop={8} className="active:opacity-60">
            <Trash2 size={20} color={ink.danger}/>
          </Pressable>
        </View>
      </View>

      {confirm && (<ExtModal title="Delete item" ink={ink}>
          <Text className="text-sm text-muted-foreground">
            Delete &quot;{item.name}&quot;? This can&apos;t be undone.
          </Text>
          <View className="flex-row gap-2">
            <View className="flex-1">
              <Button variant="outline" onPress={() => setConfirm(false)}>
                Cancel
              </Button>
            </View>
            <View className="flex-1">
              <Button variant="destructive" onPress={() => {
                setConfirm(false);
                (onDelete ?? onBack)();
            }}>
                Delete
              </Button>
            </View>
          </View>
        </ExtModal>)}

      {/* header */}
      <View className="mb-6 flex-row items-center gap-3">
        <ItemIcon url={item.url} size={56} radius={14}/>
        <View className="flex-1">
          <View className="flex-row items-center gap-2">
            <Text className="text-xl font-bold">{item.name}</Text>
            {item.favorite && <Star size={18} color={ink.star} fill={ink.star}/>}
          </View>
          <MutedText className="text-sm" numberOfLines={1}>
            {host(item.url)}
          </MutedText>
        </View>
      </View>

      {/* fields */}
      <View className="rounded-lg border border-border bg-card p-4">
        <FieldRow label="Username / Email" value={acc.username} ink={ink} copied={effCopied} copy={copy}/>
        <FieldRow label="Password" value={acc.password} ink={ink} copied={effCopied} copy={copy} secret/>
        <FieldRow label="URL" value={item.url} ink={ink} copied={effCopied} copy={copy}/>
        {acc.totp_secret && (<FieldRow label="TOTP Code" value={acc.totp_secret} ink={ink} copied={effCopied} copy={copy} totp={totp}/>)}
      </View>

      {/* notes */}
      <View className="mt-4 rounded-lg border border-border bg-card p-4">
        <Text className="mb-2 text-xs font-semibold uppercase text-muted-foreground">Notes</Text>
        {acc.notes ? (<Text className="text-sm">{acc.notes}</Text>) : (<Text className="text-sm italic text-muted-foreground">
            No notes — tap Edit to add some.
          </Text>)}
      </View>

      {/* details */}
      <View className="mt-4 rounded-lg border border-border bg-card p-4">
        <Text className="mb-2 text-xs font-semibold uppercase text-muted-foreground">Details</Text>
        <MutedText className="mb-1 text-xs">Created: {item.created}</MutedText>
        <MutedText className="text-xs">Updated: {item.updated}</MutedText>
        <View className="mt-3">
          <MutedText className="mb-1.5 text-xs">Tags</MutedText>
          <View className="flex-row flex-wrap gap-1.5">
            {item.tags.map((t) => {
            const Icon = t.icon === "circle" ? CircleIcon : t.icon === "star" ? Star : TagIcon;
            return (<View key={t.name} className="flex-row items-center gap-1 rounded-md border px-2 py-1" style={{ borderColor: `${t.color}55` }}>
                  <Icon size={12} color={t.color}/>
                  <Text className="text-xs">{t.name}</Text>
                </View>);
        })}
          </View>
        </View>
      </View>
    </ScrollView>);
}
// ── generator (entropy-ui PasswordGenerator, reproduced) ──────────────────────
function ExtBottomNav({ active, onChange, ink, }) {
    const tabs = [
        ["vault", "Login Items", Lock],
        ["tags", "Tags", Folder],
        ["send", "Send", Send],
        ["secrets", "Secrets", KeyRound],
    ];
    return (<View className="flex-row border-t border-border">
      {tabs.map(([id, label, Icon]) => {
            const on = id === active;
            return (<Pressable key={id} onPress={() => onChange(id)} className="flex-1 items-center justify-center gap-0.5 py-2 active:opacity-70">
            <Icon size={18} color={on ? ink.fg : ink.muted}/>
            <RNText style={{
                    fontSize: 10,
                    fontWeight: on ? "600" : "400",
                    color: on ? ink.fg : ink.muted,
                }}>
              {label}
            </RNText>
          </Pressable>);
        })}
    </View>);
}
function ExtComingSoon({ icon, desc, ink, }) {
    return (<View className="flex-1 items-center justify-center px-8" style={{ gap: 10 }}>
      {icon}
      <MutedText className="text-center text-sm" style={{ maxWidth: 260 }}>
        {desc}
      </MutedText>
      <RNText style={{ fontSize: 11, fontStyle: "italic", color: ink.muted }}>
        Coming soon
      </RNText>
    </View>);
}
/**
 * The mark in the popup header.
 *
 * Supplied by the caller rather than loaded from a path. The original pointed
 * at `/products/vault_light.png`, which only resolves on the marketing site —
 * a shared package cannot reach into one consumer's public directory. Falls
 * back to the wordmark so the header is never empty.
 */
export function VaultMark({ logo }) {
    if (logo) {
        return <>{logo}</>;
    }
    return <Text className="text-base font-bold">Vault</Text>;
}
function ExtHeader({ tab, ink, onAdd, onAddFolder, onSettings, logo, }) {
    return (<View className="flex-row items-center gap-3 border-b border-border px-4 py-3">
      <VaultMark logo={logo}/>
      <View className="flex-1"/>
      {tab === "tags" && (<Pressable onPress={onAddFolder} className="active:opacity-70">
          <FolderPlus size={18} color={ink.fg} strokeWidth={1.75}/>
        </Pressable>)}
      <Pressable onPress={onAdd} className="active:opacity-70">
        <Plus size={20} color={ink.fg} strokeWidth={1.75}/>
      </Pressable>
      <Pressable onPress={onSettings} className="active:opacity-70">
        <SettingsIcon size={20} color={ink.fg} strokeWidth={1.75}/>
      </Pressable>
    </View>);
}
const SEED_FOLDERS = [
    { id: "f1", name: "Personal" },
    { id: "f2", name: "Finance" },
    { id: "f3", name: "Gaming" },
];
const SEED_TAGS = [
    { id: "t1", ...T.social, folderId: "f1" },
    { id: "t2", ...T.streaming, folderId: "f1" },
    { id: "t3", ...T.finance, folderId: "f2" },
    { id: "t4", ...T.gaming, folderId: "f3" },
    { id: "t5", ...T.shopping, folderId: null },
];
function TagChip({ tag }) {
    const Icon = tag.icon === "circle" ? CircleIcon : tag.icon === "star" ? Star : TagIcon;
    return (<View className="flex-row items-center gap-1 self-start rounded-md border px-2 py-1" style={{ borderColor: `${tag.color}55` }}>
      <Icon size={11} color={tag.color}/>
      <Text className="text-xs">{tag.name}</Text>
    </View>);
}
function ExtDragHandle({ ink }) {
    // Affordance only. The website's version wires this to @dnd-kit, which is
    // DOM-only and cannot ship in a component React Native also renders. The grip
    // stays because removing it would change the extension's actual layout, which
    // is the thing this demo exists to reproduce.
    return (<View>
      <GripVertical size={14} color={ink.muted} strokeWidth={1.75}/>
    </View>);
}
function ExtTagRow({ tag, ink, onEdit, onDelete, }) {
    return (<View className="flex-row items-center gap-2 rounded-md px-1.5 py-1.5">
      <ExtDragHandle ink={ink}/>
      <View className="flex-1">
        <TagChip tag={tag}/>
      </View>
      <Pressable onPress={onEdit} hitSlop={6} className="active:opacity-60">
        <Pencil size={13} color={ink.muted} strokeWidth={1.75}/>
      </Pressable>
      <Pressable onPress={onDelete} hitSlop={6} className="active:opacity-60">
        <Trash2 size={13} color={ink.danger} strokeWidth={1.75}/>
      </Pressable>
    </View>);
}
function ExtFolderBlock({ folder, tags, collapsed, ink, onToggle, onAdd, onEdit, onDelete, onEditTag, onDeleteTag, }) {
    return (<View className="overflow-hidden rounded-lg border bg-card" style={{ borderColor: ink.border }}>
      <View className="flex-row items-center gap-2 px-2.5 py-2.5">
        <ExtDragHandle ink={ink}/>
        <Pressable onPress={onToggle} hitSlop={4} className="active:opacity-60">
          {collapsed ? (<ChevronRight size={14} color={ink.muted} strokeWidth={1.75}/>) : (<ChevronDown size={14} color={ink.muted} strokeWidth={1.75}/>)}
        </Pressable>
        <Folder size={14} color={ink.fg} strokeWidth={1.75}/>
        <Pressable onPress={onToggle} className="flex-1">
          <Text className="text-[13px] font-semibold">{folder.name}</Text>
        </Pressable>
        <Text className="text-[11px] text-muted-foreground">{tags.length}</Text>
        <Pressable onPress={onAdd} hitSlop={6} className="active:opacity-60">
          <Plus size={14} color={ink.muted} strokeWidth={1.75}/>
        </Pressable>
        <Pressable onPress={onEdit} hitSlop={6} className="active:opacity-60">
          <Pencil size={14} color={ink.muted} strokeWidth={1.75}/>
        </Pressable>
        <Pressable onPress={onDelete} hitSlop={6} className="active:opacity-60">
          <Trash2 size={14} color={ink.danger} strokeWidth={1.75}/>
        </Pressable>
      </View>
      {!collapsed && (<View className="px-2.5 pb-2.5" style={{ gap: 6 }}>
          {tags.length === 0 ? (<Text className="py-1 text-[11px] italic text-muted-foreground">
              Drop tags here, or use + to add
            </Text>) : (tags.map((t) => (<ExtTagRow key={t.id} tag={t} ink={ink} onEdit={() => onEditTag(t)} onDelete={() => onDeleteTag(t.id)}/>)))}
        </View>)}
    </View>);
}
function ExtUngrouped({ tags, ink, onEditTag, onDeleteTag, }) {
    return (<View className="mt-1 rounded-lg border bg-card p-2.5" style={{ borderColor: ink.border, gap: 6 }}>
      <Text className="text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">
        Ungrouped
      </Text>
      {tags.length === 0 ? (<Text className="py-1 text-[11px] italic text-muted-foreground">No tags</Text>) : (tags.map((t) => (<ExtTagRow key={t.id} tag={t} ink={ink} onEdit={() => onEditTag(t)} onDelete={() => onDeleteTag(t.id)}/>)))}
    </View>);
}
function ExtTagsTab({ ink }) {
    const [folders, setFolders] = useState(SEED_FOLDERS);
    const [tags, setTags] = useState(SEED_TAGS);
    const [collapsed, setCollapsed] = useState(new Set());
    const [modal, setModal] = useState(null);
    function toggle(id) {
        setCollapsed((s) => {
            const n = new Set(s);
            if (n.has(id)) {
                n.delete(id);
            }
            else {
                n.add(id);
            }
            return n;
        });
    }
    function deleteTag(id) {
        setTags((s) => s.filter((t) => t.id !== id));
    }
    function deleteFolder(id) {
        setFolders((s) => s.filter((f) => f.id !== id));
        setTags((s) => s.map((t) => (t.folderId === id ? { ...t, folderId: null } : t)));
    }
    const ungrouped = tags.filter((t) => !t.folderId);
    return (<View className="flex-1">
      <ScrollView className="flex-1" contentContainerStyle={{ padding: 12, gap: 8 }}>
        {folders.map((f) => (<ExtFolderBlock key={f.id} folder={f} tags={tags.filter((t) => t.folderId === f.id)} collapsed={collapsed.has(f.id)} ink={ink} onToggle={() => toggle(f.id)} onAdd={() => setModal({ kind: "tag" })} onEdit={() => setModal({ kind: "folder", folder: f })} onDelete={() => deleteFolder(f.id)} onEditTag={(t) => setModal({ kind: "tag", tag: t })} onDeleteTag={deleteTag}/>))}
        <ExtUngrouped tags={ungrouped} ink={ink} onEditTag={(t) => setModal({ kind: "tag", tag: t })} onDeleteTag={deleteTag}/>
      </ScrollView>
      {modal?.kind === "tag" && (<ExtTagModal ink={ink} tag={modal.tag} onClose={() => setModal(null)}/>)}
      {modal?.kind === "folder" && (<ExtFolderModal ink={ink} folder={modal.folder} onClose={() => setModal(null)}/>)}
    </View>);
}
function ExtSettingsRow({ icon, label, ink, onPress, }) {
    return (<Pressable onPress={onPress} className="flex-row items-center gap-3 rounded-lg border border-border px-3 py-3 active:bg-secondary">
      {icon}
      <Text className="flex-1 text-sm">{label}</Text>
      <ChevronRight size={16} color={ink.muted}/>
    </Pressable>);
}
// Settings flow (clients/extension SettingsScreen + PasskeyListScreen +
// ServerConfigScreen). Owns its own little sub-router so the gear is a full
// port: menu → Passkeys → Add passkey, and menu → Server URL.
function ExtSettings({ ink, onBack }) {
    const [sub, setSub] = useState("menu");
    if (sub === "passkeys") {
        return <ExtPasskeys ink={ink} onBack={() => setSub("menu")} onAdd={() => setSub("enroll")}/>;
    }
    if (sub === "enroll") {
        return <ExtEnroll ink={ink} onBack={() => setSub("passkeys")}/>;
    }
    if (sub === "server") {
        return <ExtServerConfig ink={ink} onBack={() => setSub("menu")}/>;
    }
    return (<View className="flex-1 p-5" style={{ gap: 12 }}>
      <View className="flex-row items-center justify-between">
        <VaultMark />
        <Pressable onPress={onBack} hitSlop={8} className="active:opacity-60">
          <Text className="text-[13px] font-medium text-muted-foreground">Done</Text>
        </Pressable>
      </View>
      <Text className="mt-2 text-lg font-semibold">Settings</Text>
      <ScrollView className="flex-1" contentContainerStyle={{ gap: 8, paddingBottom: 16 }}>
        <ExtSettingsRow ink={ink} icon={<KeyRound size={18} color={ink.fg} strokeWidth={1.75}/>} label="Passkeys" onPress={() => setSub("passkeys")}/>
        <ExtSettingsRow ink={ink} icon={<Server size={18} color={ink.fg} strokeWidth={1.75}/>} label="Server URL" onPress={() => setSub("server")}/>
        <View style={{ height: 8 }}/>
        <Button variant="destructive" onPress={() => { }}>
          <View className="flex-row items-center justify-center gap-2">
            <LogOut size={16} color="#ffffff" strokeWidth={2}/>
            <Text className="text-sm font-medium" style={{ color: "#ffffff" }}>
              Sign out
            </Text>
          </View>
        </Button>
        <Text className="mt-1 text-xs text-muted-foreground">
          Signing out clears your session token. Your encrypted vault stays on the server.
        </Text>
      </ScrollView>
    </View>);
}
// Passkey management (PasskeyListScreen) — popup directs rename/delete to the
// main app behind the MFA gate; "Add passkey" stays one click away.
function ExtPasskeys({ ink, onBack, onAdd, }) {
    return (<View className="flex-1 p-5" style={{ gap: 12 }}>
      <View className="flex-row items-center justify-between">
        <View className="flex-row items-center gap-2">
          <Pressable onPress={onBack} hitSlop={8} className="active:opacity-60">
            <ChevronLeft size={20} color={ink.fg} strokeWidth={1.75}/>
          </Pressable>
          <VaultMark />
        </View>
        <Text className="text-base font-semibold">Passkeys</Text>
      </View>
      <ScrollView className="flex-1" contentContainerStyle={{ gap: 16, paddingBottom: 16 }}>
        <View style={{ gap: 8 }}>
          <Button onPress={onAdd}>
            <View className="flex-row items-center justify-center gap-2">
              <Plus size={16} color={ink.bg} strokeWidth={2}/>
              <Text className="text-sm font-medium" style={{ color: ink.bg }}>
                Add passkey
              </Text>
            </View>
          </Button>
          <Text className="text-xs text-muted-foreground">
            Register a new passkey for this account using your browser&apos;s built-in WebAuthn
            dialog.
          </Text>
        </View>
        <View className="rounded-lg border border-border p-3" style={{ gap: 8 }}>
          <View className="flex-row items-center gap-2">
            <ShieldAlert size={16} color={ink.fg} strokeWidth={1.75}/>
            <Text className="flex-1 text-sm font-medium">
              Rename or remove passkeys in the main app
            </Text>
          </View>
          <Text className="text-xs leading-4 text-muted-foreground">
            Renaming and deleting passkeys requires multi-factor verification. The extension popup
            doesn&apos;t surface a TOTP prompt today, so those actions live in the main Vault app
            (web or mobile).
          </Text>
          <View className="mt-1">
            <Button variant="outline" onPress={() => { }}>
              <View className="flex-row items-center justify-center gap-2">
                <ExternalLink size={14} color={ink.fg} strokeWidth={1.75}/>
                <Text className="text-[13px] font-medium">Open the main app</Text>
              </View>
            </Button>
            <Text className="mt-1 text-[11px] text-muted-foreground" numberOfLines={1}>
              vault.example.com
            </Text>
          </View>
        </View>
      </ScrollView>
    </View>);
}
function ExtEnroll({ ink, onBack }) {
    return (<View className="flex-1">
      <View className="flex-row items-center gap-2 px-4 py-3">
        <Pressable onPress={onBack} hitSlop={8} className="active:opacity-60">
          <ChevronLeft size={20} color={ink.fg} strokeWidth={1.75}/>
        </Pressable>
        <VaultMark />
      </View>
      <ScrollView className="flex-1" contentContainerStyle={{ padding: 16 }}>
        <PasskeyEnroll ink={ink}/>
      </ScrollView>
    </View>);
}
async function probeServer(rawUrl) {
    const trimmed = rawUrl.trim();
    let parsed;
    try {
        parsed = new URL(trimmed);
    }
    catch {
        return { ok: false, message: "Invalid URL format" };
    }
    const target = `${trimmed.replace(/\/+$/, "")}/health`;
    const ctrl = new AbortController();
    const timer = setTimeout(() => ctrl.abort(), 8000);
    try {
        const res = await fetch(target, { method: "GET", credentials: "omit", signal: ctrl.signal });
        if (!res.ok) {
            return { ok: false, message: `Server responded with HTTP ${res.status}` };
        }
        let body = {};
        try {
            body = (await res.json());
        }
        catch {
            return { ok: false, message: "Unexpected response body" };
        }
        if (body.status !== "healthy") {
            return { ok: false, message: `Server reported status: ${body.status ?? "unknown"}` };
        }
        return {
            ok: true,
            message: body.version ? `Server reachable (version ${body.version})` : "Server reachable",
        };
    }
    catch (e) {
        const reason = e instanceof Error ? e.message : "network error";
        if (parsed.protocol === "https:" && reason.toLowerCase().includes("certificate")) {
            return { ok: false, message: "TLS certificate validation failed" };
        }
        if (reason.toLowerCase().includes("abort") || reason.toLowerCase().includes("timeout")) {
            return { ok: false, message: "Request timed out" };
        }
        return { ok: false, message: `Could not reach server (${reason})` };
    }
    finally {
        clearTimeout(timer);
    }
}
function ExtServerConfig({ ink, onBack }) {
    const DEFAULT_URL = "https://vault.example.com/api";
    const [url, setUrl] = useState(DEFAULT_URL);
    const [testing, setTesting] = useState(false);
    const [result, setResult] = useState(null);
    async function test() {
        setTesting(true);
        setResult(null);
        const r = await probeServer(url);
        setResult(r);
        setTesting(false);
    }
    return (<ScrollView className="flex-1" contentContainerStyle={{ padding: 24, gap: 16 }}>
      <View className="items-start" style={{ marginBottom: 8 }}>
        <VaultMark />
      </View>
      <View style={{ gap: 4, marginBottom: 4 }}>
        <Text className="text-[22px] font-semibold">Server URL</Text>
        <Text className="text-[13px] text-muted-foreground">
          Point this extension at your self-hosted Vault server. HTTPS required (loopback may use
          HTTP).
        </Text>
      </View>
      <ExtField label="URL" placeholder="https://vault.example.com/api" value={url} onChange={(v) => {
            setUrl(v);
            setResult(null);
        }} ink={ink}/>
      {result && (<View className="rounded-lg border p-2.5" style={{
                borderColor: result.ok ? "rgb(34,197,94)" : ink.danger,
                backgroundColor: result.ok ? "rgba(34,197,94,0.08)" : "rgba(239,68,68,0.08)",
            }}>
          <Text className="text-[13px] font-medium" style={{ color: result.ok ? "rgb(34,197,94)" : ink.danger }}>
            {result.ok ? "✓ " : "✗ "}
            {result.message}
          </Text>
        </View>)}
      <View className="flex-row gap-2">
        <View className="flex-1">
          <Button variant="outline" onPress={test} isLoading={testing}>
            {testing ? "" : "Test"}
          </Button>
        </View>
        <View className="flex-1">
          <Button onPress={onBack}>Save</Button>
        </View>
      </View>
      <Button variant="ghost" onPress={() => {
            setUrl(DEFAULT_URL);
            setResult(null);
        }}>
        Reset to default
      </Button>
      <Pressable onPress={onBack} className="items-center active:opacity-60" style={{ marginTop: 4 }}>
        <Text className="text-[13px] text-muted-foreground">Back</Text>
      </Pressable>
    </ScrollView>);
}
function ExtField({ label, placeholder, value, onChange, ink, secret, }) {
    const [show, setShow] = useState(false);
    return (<View style={{ gap: 6 }}>
      <Text className="text-xs text-muted-foreground">{label}</Text>
      <View className="flex-row items-center rounded-lg border border-border px-3" style={{ height: 40 }}>
        <TextInput value={value} onChangeText={onChange} placeholder={placeholder} placeholderTextColor={ink.muted} secureTextEntry={secret && !show} className="flex-1 text-sm text-foreground" style={{ outlineStyle: "none" }}/>
        {secret && (<Pressable onPress={() => setShow((s) => !s)} className="active:opacity-60">
            {show ? <EyeOff size={16} color={ink.muted}/> : <Eye size={16} color={ink.muted}/>}
          </Pressable>)}
      </View>
    </View>);
}
// New / edit login form (clients/extension VaultItemFormScreen). With `item`
// the fields prefill and the screen acts as the editor.
function ExtItemForm({ ink, onCancel, item, }) {
    const acc = item?.accounts[0];
    const [fav, setFav] = useState(item?.favorite ?? false);
    const [picked, setPicked] = useState(item?.tags.map((t) => t.name) ?? []);
    const [f, setF] = useState({
        name: item?.name ?? "",
        url: item?.url ?? "",
        label: "",
        username: acc?.username ?? "",
        password: acc?.password ?? "",
        totp: acc?.totp_secret ?? "",
        notes: acc?.notes ?? "",
    });
    const set = (k) => (v) => setF((s) => ({ ...s, [k]: v }));
    const allTags = Object.values(T);
    return (<View className="flex-1">
      <View className="flex-row items-center justify-between border-b border-border px-4 py-3">
        <Pressable onPress={onCancel} className="active:opacity-70">
          <ArrowLeft size={20} color={ink.fg}/>
        </Pressable>
        <Text className="text-base font-bold">{item ? "Edit item" : "New item"}</Text>
        <Pressable onPress={() => setFav((v) => !v)} className="active:opacity-70">
          <Star size={20} color={fav ? ink.star : ink.muted} fill={fav ? ink.star : "transparent"}/>
        </Pressable>
      </View>
      <ScrollView className="flex-1" contentContainerStyle={{ padding: 16, gap: 14 }}>
        <ExtField label="Name" placeholder="e.g. Google" value={f.name} onChange={set("name")} ink={ink}/>
        <ExtField label="URL" placeholder="https://example.com" value={f.url} onChange={set("url")} ink={ink}/>
        <View style={{ gap: 6 }}>
          <Text className="text-xs text-muted-foreground">Tags</Text>
          <View className="flex-row flex-wrap gap-1.5">
            {allTags.map((t) => {
            const on = picked.includes(t.name);
            const Icon = t.icon === "circle" ? CircleIcon : t.icon === "star" ? Star : TagIcon;
            return (<Pressable key={t.name} onPress={() => setPicked((s) => (on ? s.filter((x) => x !== t.name) : [...s, t.name]))} className="flex-row items-center gap-1 rounded-md border px-2 py-1" style={{
                    borderColor: `${t.color}${on ? "" : "55"}`,
                    backgroundColor: on ? `${t.color}22` : "transparent",
                }}>
                  <Icon size={11} color={t.color}/>
                  <Text className="text-xs">{t.name}</Text>
                </Pressable>);
        })}
          </View>
        </View>
        <View className="rounded-lg border border-border bg-card p-3" style={{ gap: 12 }}>
          <ExtField label="Label" placeholder="e.g. Personal, Work" value={f.label} onChange={set("label")} ink={ink}/>
          <ExtField label="Username" placeholder="you@example.com" value={f.username} onChange={set("username")} ink={ink}/>
          <ExtField label="Password" placeholder="••••••••" value={f.password} onChange={set("password")} ink={ink} secret/>
          <ExtField label="TOTP secret (base32)" placeholder="JBSWY3DPEHPK3PXP" value={f.totp} onChange={set("totp")} ink={ink}/>
          <ExtField label="Notes" placeholder="Optional" value={f.notes} onChange={set("notes")} ink={ink}/>
        </View>
        <Pressable onPress={() => { }} className="flex-row items-center justify-center gap-2 rounded-lg border border-border py-2.5 active:opacity-80">
          <Plus size={16} color={ink.fg}/>
          <Text className="text-sm font-medium">Add account</Text>
        </Pressable>
        <View className="flex-row gap-2 pt-1">
          <View className="flex-1">
            <Button variant="outline" onPress={onCancel}>
              Cancel
            </Button>
          </View>
          <View className="flex-1">
            <Button onPress={onCancel}>{item ? "Save changes" : "Create"}</Button>
          </View>
        </View>
      </ScrollView>
    </View>);
}
function ExtModal({ title, ink, children, }) {
    void ink;
    return (<View className="absolute inset-0 z-50 items-center justify-center p-5" style={{ backgroundColor: "rgba(0,0,0,0.4)" }}>
      <View className="w-full rounded-xl border border-border bg-card p-4" style={{ maxWidth: 320, gap: 14 }}>
        <Text className="text-sm font-bold">{title}</Text>
        {children}
      </View>
    </View>);
}
const TAG_COLORS = ["#3b82f6", "#22c55e", "#f59e0b", "#a855f7", "#06b6d4", "#ef4444"];
function ExtTagModal({ ink, onClose, tag, }) {
    const [name, setName] = useState(tag?.name ?? "");
    const [color, setColor] = useState(tag?.color ?? TAG_COLORS[0]);
    return (<ExtModal title={tag ? "Edit tag" : "New tag"} ink={ink}>
      <ExtField label="Name" placeholder="Personal" value={name} onChange={setName} ink={ink}/>
      <View style={{ gap: 6 }}>
        <Text className="text-xs text-muted-foreground">Color</Text>
        <View className="flex-row gap-2">
          {TAG_COLORS.map((c) => (<Pressable key={c} onPress={() => setColor(c)} style={{
                width: 28,
                height: 28,
                borderRadius: 999,
                backgroundColor: c,
                borderWidth: color === c ? 2 : 0,
                borderColor: ink.fg,
            }}/>))}
        </View>
      </View>
      <View className="flex-row gap-2">
        <View className="flex-1">
          <Button variant="outline" onPress={onClose}>
            Cancel
          </Button>
        </View>
        <View className="flex-1">
          <Button onPress={onClose}>{tag ? "Save" : "Create"}</Button>
        </View>
      </View>
    </ExtModal>);
}
function ExtFolderModal({ ink, onClose, folder, }) {
    const [name, setName] = useState(folder?.name ?? "");
    return (<ExtModal title={folder ? "Edit folder" : "New folder"} ink={ink}>
      <ExtField label="Name" placeholder="Work" value={name} onChange={setName} ink={ink}/>
      <View className="flex-row gap-2">
        <View className="flex-1">
          <Button variant="outline" onPress={onClose}>
            Cancel
          </Button>
        </View>
        <View className="flex-1">
          <Button onPress={onClose}>{folder ? "Save" : "Create"}</Button>
        </View>
      </View>
    </ExtModal>);
}
// External controller — lets a parent scene drive the popup (route, the
// highlighted row, the "copied" field, and the list/detail scroll positions)
// instead of the popup's own internal autoplay. Used by TotpScene so a single
// scene-level cursor can hand the code off to a browser window.
function PasskeyEnroll({ ink }) {
    const [label, setLabel] = useState("My iPhone");
    return (<Card className="w-full max-w-[480px] self-center" style={vaultVars(ink.fg === "#ffffff")}>
      <CardHeader className="items-center">
        <View className="mb-3 h-16 w-16 items-center justify-center rounded-full bg-secondary">
          <KeyRound size={30} color={ink.fg}/>
        </View>
        <CardTitle className="text-center">Add a passkey</CardTitle>
        <CardDescription className="text-center">
          Passkeys are a faster, phishing-resistant way to sign in. Use Face ID, Touch ID, or a
          hardware key.
        </CardDescription>
      </CardHeader>
      <CardContent className="gap-4">
        <View className="flex-row items-start gap-2 rounded-lg bg-secondary p-3">
          <View className="mt-0.5">
            <Fingerprint size={16} color={ink.muted}/>
          </View>
          <MutedText className="flex-1 text-xs leading-4">
            Your passkey stays on this device (or in iCloud Keychain / Google Password Manager).
            Vault never sees the private key.
          </MutedText>
        </View>
        <View className="gap-1.5">
          <Text className="text-xs text-muted-foreground">Passkey name</Text>
          <View className="rounded-lg border border-border px-3 py-2.5">
            <TextInput value={label} onChangeText={setLabel} placeholder="My iPhone" placeholderTextColor={ink.muted} className="text-sm text-foreground" style={{ outlineStyle: "none" }}/>
          </View>
          <MutedText className="text-xs">
            Used to identify this passkey in your settings list.
          </MutedText>
        </View>
      </CardContent>
      <CardFooter className="flex-col gap-2 border-t-0">
        <Button className="w-full">Create passkey</Button>
      </CardFooter>
    </Card>);
}
/**
 * The browser-extension popup.
 *
 * Interactive by default. Pass `external` to have a parent scene drive it.
 */
export function ExtensionPopup({ logo, external, height = EXTENSION_POPUP_HEIGHT, } = {}) {
    const ink = useInk();
    const dark = ink.fg === "#ffffff";
    const [tab, setTab] = useState("vault");
    const [route, setRoute] = useState({ r: "main" });
    const [modal, setModal] = useState(null);
    const [items, setItems] = useState(ITEMS);
    const driven = !!external;
    const vroute = external ? external.route : route;
    const openItem = vroute.r === "detail" ? (items.find((i) => i.id === vroute.id) ?? null) : null;
    return (<View className="relative self-center overflow-hidden rounded-xl border border-border bg-background" style={[vaultVars(dark), { width: EXTENSION_POPUP_WIDTH, height }]} 
    // A driven popup is a picture, not a control: taps would fight the scene.
    pointerEvents={driven ? "none" : "auto"}>
      {vroute.r === "detail" && openItem ? (<ItemDetail item={openItem} ink={ink} onBack={() => setRoute({ r: "main" })} onDelete={() => {
                const id = openItem.id;
                setItems((s) => s.filter((i) => i.id !== id));
                setRoute({ r: "main" });
            }} forceCopiedLabel={driven ? external?.autoCopied : undefined} scrollRef={external?.detailScrollRef}/>) : vroute.r === "settings" ? (<ExtSettings ink={ink} onBack={() => setRoute({ r: "main" })}/>) : vroute.r === "new" ? (<ExtItemForm ink={ink} onCancel={() => setRoute({ r: "main" })}/>) : (<View className="flex-1">
          {/* Vault + Tags tabs show the logo header (matches the app); Send and
                Secrets are full-screen "coming soon" with no header. */}
          {(tab === "vault" || tab === "tags") && (<ExtHeader logo={logo} tab={tab} ink={ink} onAdd={() => (tab === "tags" ? setModal("tag") : setRoute({ r: "new" }))} onAddFolder={() => setModal("folder")} onSettings={() => setRoute({ r: "settings" })}/>)}

          <View className="flex-1">
            {tab === "vault" && (<VaultList ink={ink} items={items} onOpen={(id) => setRoute({ r: "detail", id })} scrollRef={external?.listScrollRef} highlightId={external?.highlightId}/>)}
            {tab === "tags" && <ExtTagsTab ink={ink}/>}
            {tab === "send" && (<ExtComingSoon ink={ink} icon={<Send size={28} color={ink.muted}/>} desc="Send files securely via link or email with permissions, auto-expiry, and encryption."/>)}
            {tab === "secrets" && (<ExtComingSoon ink={ink} icon={<KeyRound size={28} color={ink.muted}/>} desc="Manage API keys, tokens, environment variables, and other developer secrets."/>)}
          </View>

          <ExtBottomNav active={tab} onChange={(t) => {
                setTab(t);
                setRoute({ r: "main" });
            }} ink={ink}/>

          {modal === "tag" && <ExtTagModal ink={ink} onClose={() => setModal(null)}/>}
          {modal === "folder" && <ExtFolderModal ink={ink} onClose={() => setModal(null)}/>}
        </View>)}
    </View>);
}
export default ExtensionPopup;
//# sourceMappingURL=extension-popup.jsx.map