{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "byok-setup",
  "title": "BYOK Setup",
  "description": "A bring-your-own-key card — configurable provider slots, a masked field that cleans what you paste and checks the shape per provider, a cancellable live test with latency or a reason, a storage disclaimer that never leaves, and removal behind a confirm.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "alert-dialog",
    "badge",
    "button",
    "utils"
  ],
  "files": [
    {
      "path": "src/registry/ui/byok-setup.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n  ArrowRight,\n  CircleCheck,\n  ExternalLink,\n  Eye,\n  EyeOff,\n  HardDrive,\n  KeyRound,\n  LoaderCircle,\n  Plug,\n  RefreshCw,\n  ShieldCheck,\n  Timer,\n  Trash2,\n  TriangleAlert,\n  type LucideIcon,\n} from \"lucide-react\"\n\nimport {\n  AlertDialog,\n  AlertDialogCancel,\n  AlertDialogContent,\n  AlertDialogDescription,\n  AlertDialogFooter,\n  AlertDialogHeader,\n  AlertDialogTitle,\n  AlertDialogTrigger,\n} from \"@/components/ui/alert-dialog\"\nimport { Badge } from \"@/components/ui/badge\"\nimport { Button } from \"@/components/ui/button\"\nimport { cn } from \"@/lib/utils\"\n\n/* -------------------------------------------------------------------------- *\n * Providers — the slots\n *\n * Which providers a product accepts is deployment configuration, not markup:\n * one entry per slot, so a build that only takes Anthropic keys is a prop and\n * not a fork. Everything the UI needs in order to be specific about a key —\n * the prefix it starts with, the shape it has, where it is issued, what the\n * account behind it needs — travels with the slot.\n * -------------------------------------------------------------------------- */\n\nexport interface ByokProvider {\n  /** Stable slot id. Handed back by every callback. */\n  id: string\n  /** Shown on the chip and in every sentence about this slot. */\n  name: string\n  /** Literal prefix every key of this provider starts with — checked before `pattern`, so the rejection can name it. */\n  keyPrefix?: string\n  /** Whole-key shape. Anchor it yourself (`^…$`); `g`/`y` flags are stripped before use. */\n  pattern?: RegExp\n  /** Shortest plausible key. Catches a truncated paste before a request is spent on it. */\n  minLength?: number\n  placeholder?: string\n  /** One line describing the expected shape, shown when the key is rejected. */\n  formatHint?: string\n  /** Where the key is issued — rendered as an external link beside the field. */\n  consoleUrl?: string\n  /** Label for that link (default \"Get a key\"). */\n  consoleLabel?: string\n  /** Standing note about the account rather than the key: billing, waitlists, regions. */\n  note?: React.ReactNode\n}\n\nexport const DEFAULT_BYOK_PROVIDERS: ByokProvider[] = [\n  {\n    id: \"openai\",\n    name: \"OpenAI\",\n    keyPrefix: \"sk-\",\n    // The negative lookahead is what makes a mis-paste detectable: two of the\n    // slots below also issue `sk-…` keys, and a pattern that accepted them\n    // would happily call an Anthropic key a valid OpenAI one.\n    pattern: /^sk-(?!ant-|or-)[A-Za-z0-9_-]{20,}$/,\n    minLength: 24,\n    placeholder: \"sk-proj-…\",\n    formatHint: \"An OpenAI key starts with sk- and contains no spaces.\",\n    consoleUrl: \"https://platform.openai.com/api-keys\",\n    note: \"Needs a funded account — a key with no credit answers 429 on the very first call.\",\n  },\n  {\n    id: \"anthropic\",\n    name: \"Anthropic\",\n    keyPrefix: \"sk-ant-\",\n    pattern: /^sk-ant-[A-Za-z0-9_-]{24,}$/,\n    minLength: 32,\n    placeholder: \"sk-ant-api03-…\",\n    formatHint: \"An Anthropic key starts with sk-ant-.\",\n    consoleUrl: \"https://console.anthropic.com/settings/keys\",\n  },\n  {\n    id: \"google\",\n    name: \"Google AI Studio\",\n    keyPrefix: \"AIza\",\n    pattern: /^AIza[A-Za-z0-9_-]{30,}$/,\n    minLength: 34,\n    placeholder: \"AIza…\",\n    formatHint: \"A Google AI Studio key starts with AIza.\",\n    consoleUrl: \"https://aistudio.google.com/apikey\",\n  },\n  {\n    id: \"openrouter\",\n    name: \"OpenRouter\",\n    keyPrefix: \"sk-or-\",\n    pattern: /^sk-or-v1-[A-Za-z0-9]{32,}$/,\n    minLength: 40,\n    placeholder: \"sk-or-v1-…\",\n    formatHint: \"An OpenRouter key starts with sk-or-v1-.\",\n    consoleUrl: \"https://openrouter.ai/keys\",\n  },\n]\n\nconst FALLBACK_PROVIDER: ByokProvider = { id: \"\", name: \"this provider\" }\n\n/**\n * What lands in the field is rarely just the key: consoles hand back a quoted\n * string, terminals wrap it across lines, docs prefix it with `Authorization:\n * Bearer`. None of that is part of a key, and all of it turns a working key\n * into a 401 nobody can explain — so it is stripped on the way in instead of\n * being sent and blamed on the user.\n */\nexport function normalizeByokKey(raw: string): string {\n  let key = raw.trim()\n  key = key.replace(/^authorization:\\s*/i, \"\")\n  key = key.replace(/^bearer\\s+/i, \"\")\n  key = key.replace(/\\s+/g, \"\")\n  if (key.length >= 2) {\n    const first = key[0]\n    const last = key[key.length - 1]\n    if ((first === '\"' && last === '\"') || (first === \"'\" && last === \"'\")) key = key.slice(1, -1)\n  }\n  return key\n}\n\n/** `sk-pro…4f9c` — what is safe to keep on screen once the real key lives somewhere else. */\nexport function byokKeyHint(key: string): string {\n  const tail = key.length > 8 ? key.slice(-4) : \"\"\n  const head = key.slice(0, Math.max(0, Math.min(6, key.length - tail.length)))\n  return `${head}…${tail}`\n}\n\n/** A `g`/`y` pattern carries `lastIndex` between calls, so the same string tests true, then false. */\nfunction stateless(pattern: RegExp): RegExp {\n  return /[gy]/.test(pattern.flags) ? new RegExp(pattern.source, pattern.flags.replace(/[gy]/g, \"\")) : pattern\n}\n\nfunction looksLike(key: string, provider: ByokProvider): boolean {\n  if (provider.pattern) return stateless(provider.pattern).test(key)\n  if (provider.keyPrefix) {\n    return key.startsWith(provider.keyPrefix) && key.length >= (provider.minLength ?? provider.keyPrefix.length + 8)\n  }\n  return false\n}\n\n/** The slot this key really belongs to. The most specific prefix wins, because every `sk-ant-` key is also an `sk-` key. */\nfunction suggestSlot(key: string, providers: ByokProvider[], currentId: string): ByokProvider | undefined {\n  let best: ByokProvider | undefined\n  for (const candidate of providers) {\n    if (candidate.id === currentId || !looksLike(key, candidate)) continue\n    if (!best || (candidate.keyPrefix?.length ?? 0) > (best.keyPrefix?.length ?? 0)) best = candidate\n  }\n  return best\n}\n\nexport type ByokKeyProblem = \"empty\" | \"prefix\" | \"short\" | \"shape\"\n\nexport interface ByokKeyCheck {\n  ok: boolean\n  problem?: ByokKeyProblem\n  message?: string\n  /** Set when the key matches a DIFFERENT configured slot — a paste into the wrong provider. */\n  suggestion?: ByokProvider\n}\n\n/**\n * Local checking is a courtesy, never a verdict: it only catches what can be\n * known without spending a request (wrong prefix, truncated paste, wrong\n * slot). Whether the key actually WORKS is a question only the provider\n * answers, which is what the test button is for.\n */\nexport function checkByokKey(key: string, provider: ByokProvider, providers: ByokProvider[]): ByokKeyCheck {\n  if (!key) return { ok: false, problem: \"empty\" }\n  const suggestion = suggestSlot(key, providers, provider.id)\n\n  if (provider.keyPrefix && !key.startsWith(provider.keyPrefix)) {\n    return { ok: false, problem: \"prefix\", message: `${provider.name} keys start with ${provider.keyPrefix}`, suggestion }\n  }\n  if (provider.minLength !== undefined && key.length < provider.minLength) {\n    return {\n      ok: false,\n      problem: \"short\",\n      message: `Too short for a ${provider.name} key — ${key.length} characters, expected at least ${provider.minLength}.`,\n      suggestion,\n    }\n  }\n  if (provider.pattern && !stateless(provider.pattern).test(key)) {\n    return {\n      ok: false,\n      problem: \"shape\",\n      // \"An OpenAI key starts with sk-\" is useless advice for a key that\n      // starts with sk-ant-: when another slot claims it, say THAT instead.\n      message: suggestion\n        ? `That is a key for ${suggestion.name}, not ${provider.name}.`\n        : (provider.formatHint ?? `That does not look like a ${provider.name} key.`),\n      suggestion,\n    }\n  }\n  return { ok: true }\n}\n\n/* -------------------------------------------------------------------------- *\n * Test lifecycle\n * -------------------------------------------------------------------------- */\n\nexport type ByokFailureCode = \"auth\" | \"network\" | \"quota\" | \"region\" | \"timeout\" | \"unknown\"\n\nexport type ByokTestResult =\n  | { ok: true; latencyMs?: number; detail?: string }\n  | { ok: false; reason: string; code?: ByokFailureCode; hint?: string }\n\n/** The panel under the field, as one value. Pass `test` to own it; leave it off and the component runs the lifecycle. */\nexport type ByokTestState = { phase: \"idle\" } | { phase: \"testing\" } | { phase: \"done\"; result: ByokTestResult }\n\nexport interface ByokTestInput {\n  providerId: string\n  /** The normalized key in the field, or `null` for \"the key you already stored\" — after a save this component holds no plaintext. */\n  key: string | null\n  /** Aborted when the test is cancelled, superseded, timed out, or the component unmounts. */\n  signal?: AbortSignal\n}\n\nconst IDLE: ByokTestState = { phase: \"idle\" }\n\n/** Advice about the CLASS of failure — the provider's own wording goes in `reason`. */\nconst FAILURE_HINT: Record<ByokFailureCode, string | undefined> = {\n  auth: \"The provider rejected the key. Check it was copied whole and has not been revoked or rotated.\",\n  network: \"The provider could not be reached at all — a proxy, a firewall or an offline tab will do this.\",\n  quota: \"The key is valid but the account has nothing left to spend. Add credit, then test again.\",\n  region: \"The key is valid but not enabled for this model or region on that account.\",\n  timeout:\n    \"No answer in time. One slow first call is normal; repeated timeouts usually mean something is blocking the request.\",\n  unknown: undefined,\n}\n\n/** Past this, a successful test is worth flagging as slow instead of quietly celebrating. */\nconst SLOW_MS = 1500\n\nfunction elapsed(): number {\n  return typeof performance !== \"undefined\" ? performance.now() : Date.now()\n}\n\nfunction errorText(error: unknown): string {\n  if (error instanceof Error && error.message) return error.message\n  if (typeof error === \"string\" && error) return error\n  return \"The test failed before it reached the provider.\"\n}\n\n/* -------------------------------------------------------------------------- *\n * Storage disclaimer\n *\n * Someone is about to paste a credential billed to their own card. Where it\n * ends up is not fine print, so the line stays on screen in every mode —\n * including after the key is stored, when it is the only reminder left.\n * -------------------------------------------------------------------------- */\n\nexport type ByokStorageScope = \"device\" | \"server\" | \"session\"\n\nconst SCOPE_COPY: Record<ByokStorageScope, string> = {\n  device:\n    \"Stored in this browser only. The key is never sent to our servers, and clearing site data for this domain deletes it.\",\n  server:\n    \"Stored encrypted on our servers and used only to sign the requests you make. It is never written to logs and never shared.\",\n  session: \"Kept in memory for this tab only. Closing the tab forgets it, so you will paste it again next time.\",\n}\n\nconst SCOPE_ICON: Record<ByokStorageScope, LucideIcon> = {\n  device: HardDrive,\n  server: ShieldCheck,\n  session: Timer,\n}\n\nconst SCOPE_WHERE: Record<ByokStorageScope, string> = {\n  device: \"from this browser\",\n  server: \"from our servers\",\n  session: \"from this tab\",\n}\n\n/* -------------------------------------------------------------------------- *\n * Connection — what is left once the secret is gone\n * -------------------------------------------------------------------------- */\n\nexport interface ByokConnection {\n  providerId: string\n  /** Masked remnant of the stored key (`sk-pro…4f9c`). The real secret never comes back to the client. */\n  keyHint: string\n  /** Already formatted — this component never formats dates itself. */\n  addedAtLabel?: string\n  /** Already formatted \"last verified\" label. */\n  lastVerifiedLabel?: string\n}\n\n/* -------------------------------------------------------------------------- *\n * Provider picker — a radiogroup, not a dropdown\n *\n * Three or four slots that all fit on screen are radios: one tab stop, arrows\n * move the selection, every chip announces itself as checked or not. A\n * dropdown would hide the answer to \"which providers can I even use here\".\n * -------------------------------------------------------------------------- */\n\nfunction ProviderPicker({\n  disabled,\n  labelledBy,\n  onSelect,\n  providers,\n  value,\n}: {\n  disabled: boolean\n  labelledBy: string\n  onSelect: (id: string) => void\n  providers: ByokProvider[]\n  value: string\n}) {\n  const chips = React.useRef(new Map<string, HTMLButtonElement>())\n\n  const setChip = (id: string) => (node: HTMLButtonElement | null) => {\n    if (node) chips.current.set(id, node)\n    else chips.current.delete(id)\n  }\n\n  const moveTo = (index: number) => {\n    const next = providers[(index + providers.length) % providers.length]\n    if (!next) return\n    // Arrows move focus and selection together — the radiogroup pattern.\n    chips.current.get(next.id)?.focus()\n    onSelect(next.id)\n  }\n\n  const handleKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>, index: number) => {\n    if (disabled) return\n    switch (event.key) {\n      case \"ArrowRight\":\n      case \"ArrowDown\":\n        event.preventDefault()\n        moveTo(index + 1)\n        break\n      case \"ArrowLeft\":\n      case \"ArrowUp\":\n        event.preventDefault()\n        moveTo(index - 1)\n        break\n      case \"Home\":\n        event.preventDefault()\n        moveTo(0)\n        break\n      case \"End\":\n        event.preventDefault()\n        moveTo(providers.length - 1)\n        break\n      default:\n        break\n    }\n  }\n\n  return (\n    <div aria-labelledby={labelledBy} className=\"flex flex-wrap gap-1.5\" role=\"radiogroup\">\n      {providers.map((provider, index) => {\n        const selected = provider.id === value\n        return (\n          <button\n            aria-checked={selected}\n            className={cn(\n              \"inline-flex h-7 items-center gap-1.5 rounded-lg border px-2.5 text-xs font-medium transition-colors outline-none\",\n              \"focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50\",\n              \"disabled:pointer-events-none disabled:opacity-50\",\n              selected\n                ? \"border-primary/40 bg-primary/10 text-foreground\"\n                : \"border-border text-muted-foreground hover:bg-muted hover:text-foreground\",\n            )}\n            disabled={disabled}\n            key={provider.id}\n            onClick={() => onSelect(provider.id)}\n            onKeyDown={event => handleKeyDown(event, index)}\n            ref={setChip(provider.id)}\n            role=\"radio\"\n            // One tab stop for the whole group; the arrows do the rest.\n            tabIndex={selected ? 0 : -1}\n            type=\"button\"\n          >\n            <span\n              aria-hidden=\"true\"\n              className={cn(\"size-1.5 shrink-0 rounded-full\", selected ? \"bg-primary\" : \"bg-muted-foreground/40\")}\n            />\n            {provider.name}\n          </button>\n        )\n      })}\n    </div>\n  )\n}\n\n/* -------------------------------------------------------------------------- *\n * Component\n * -------------------------------------------------------------------------- */\n\nexport interface ByokSetupProps\n  extends Omit<React.FormHTMLAttributes<HTMLFormElement>, \"onSubmit\" | \"onChange\" | \"children\" | \"title\"> {\n  /** The slots this deployment accepts. Defaults to OpenAI / Anthropic / Google / OpenRouter. */\n  providers?: ByokProvider[]\n  /** Controlled slot selection. */\n  providerId?: string\n  defaultProviderId?: string\n  onProviderChange?: (providerId: string) => void\n  /** Initial contents of the key field for the initially selected slot — a restored draft. */\n  defaultKey?: string\n  /** Observe the normalized draft. The plaintext otherwise stays inside this component on purpose. */\n  onKeyChange?: (key: string) => void\n  /** The key already on file. `null` = nothing stored. Controlled when passed. */\n  connection?: ByokConnection | null\n  defaultConnection?: ByokConnection | null\n  /** Controlled test panel. Leave it off to let the component own the lifecycle. */\n  test?: ByokTestState\n  /** Runs one live check. Return a verdict or a promise of one; `signal` aborts on cancel / supersede / timeout / unmount. */\n  onTest?: (input: ByokTestInput) => ByokTestResult | Promise<ByokTestResult>\n  /** Only needed when `test` is controlled — the internal lifecycle cancels itself. */\n  onTestCancel?: () => void\n  /** Fires once per Save. Return a promise to drive the pending state. */\n  onSave?: (input: { providerId: string; key: string }) => void | Promise<void>\n  /** Fires once the destructive confirm is accepted. Return a promise to keep the dialog pending. */\n  onRemove?: (providerId: string) => void | Promise<void>\n  /** Block Save until a test has passed (default true). */\n  requireTestBeforeSave?: boolean\n  /** ms before an unanswered test is abandoned. 0 disables the timeout. */\n  testTimeout?: number\n  /** Picks the default disclaimer sentence and its icon. */\n  storageScope?: ByokStorageScope\n  /** Replaces the disclaimer text. Say where the key lives — never drop the line. */\n  storageNote?: React.ReactNode\n  title?: React.ReactNode\n  description?: React.ReactNode\n  /** Everything inert and dimmed — a member without permission to change billing credentials. */\n  disabled?: boolean\n}\n\nexport const ByokSetup = React.forwardRef<HTMLFormElement, ByokSetupProps>(function ByokSetup(\n  {\n    providers = DEFAULT_BYOK_PROVIDERS,\n    providerId: providerIdProp,\n    defaultProviderId,\n    onProviderChange,\n    defaultKey,\n    onKeyChange,\n    connection: connectionProp,\n    defaultConnection = null,\n    test: testProp,\n    onTest,\n    onTestCancel,\n    onSave,\n    onRemove,\n    requireTestBeforeSave = true,\n    testTimeout = 15000,\n    storageScope = \"device\",\n    storageNote,\n    title = \"Use your own API key\",\n    description = \"Requests are billed to your provider account, at their prices and their rate limits.\",\n    disabled = false,\n    className,\n    ...props\n  },\n  ref,\n) {\n  const uid = React.useId()\n  const titleId = `${uid}-title`\n  const providerLabelId = `${uid}-providers`\n  const keyId = `${uid}-key`\n  const problemId = `${uid}-problem`\n  const noteId = `${uid}-note`\n  const gateId = `${uid}-gate`\n\n  const inputRef = React.useRef<HTMLInputElement>(null)\n  const mountedRef = React.useRef(true)\n  /** Caret to restore after normalization deleted characters under the cursor. */\n  const caretRef = React.useRef<number | null>(null)\n  /** Focus to hand out once the panel that held it is gone. */\n  const focusKeyRef = React.useRef(false)\n  /** Kept separate from `focusKeyRef` because Radix may restore focus after the layout effect already cleared it. */\n  const suppressReturnFocusRef = React.useRef(false)\n  /** One-shot locks: a double click must never fire a callback twice. */\n  const saveLockRef = React.useRef(false)\n  const removeLockRef = React.useRef(false)\n  const requestSeqRef = React.useRef(0)\n  const inFlightRef = React.useRef<{\n    id: number\n    controller: AbortController | null\n    timer: number | null\n  } | null>(null)\n\n  const [internalProviderId, setInternalProviderId] = React.useState(() => defaultProviderId ?? providers[0]?.id ?? \"\")\n  /**\n   * One draft per slot. A key belongs to the provider that issued it, so\n   * switching chips must not carry it over — and switching back must not have\n   * thrown it away either.\n   */\n  const [drafts, setDrafts] = React.useState<Record<string, string>>(() => {\n    const initial = defaultProviderId ?? providers[0]?.id ?? \"\"\n    const key = defaultKey ? normalizeByokKey(defaultKey) : \"\"\n    return key && initial ? { [initial]: key } : {}\n  })\n  const [internalConnection, setInternalConnection] = React.useState<ByokConnection | null>(defaultConnection)\n  const [internalTest, setInternalTest] = React.useState<ByokTestState>(IDLE)\n  const [revealed, setRevealed] = React.useState(false)\n  const [replacing, setReplacing] = React.useState(false)\n  const [saving, setSaving] = React.useState(false)\n  const [removing, setRemoving] = React.useState(false)\n  const [removeOpen, setRemoveOpen] = React.useState(false)\n  const [actionError, setActionError] = React.useState<string | null>(null)\n  /** The user has typed at least once — before that a prefilled draft counts as already committed. */\n  const [interacted, setInteracted] = React.useState(false)\n  /** Blurred or submitted at least once — after that, validation speaks on every keystroke. */\n  const [committed, setCommitted] = React.useState(false)\n  /** The exact draft that came out of a paste we had to clean. Self-invalidating: no timer, no effect. */\n  const [cleanedFor, setCleanedFor] = React.useState<string | null>(null)\n\n  const hasProviders = providers.length > 0\n  const selectedId = providerIdProp ?? internalProviderId\n  const connection = connectionProp !== undefined ? connectionProp : internalConnection\n  /** Non-null exactly when the card is showing a stored key instead of the entry form. */\n  const stored = replacing ? null : connection\n  const entryMode = stored === null\n\n  const activeProviderId = stored ? stored.providerId : selectedId\n  const provider = providers.find(candidate => candidate.id === activeProviderId) ?? providers[0] ?? FALLBACK_PROVIDER\n  const draftKey = drafts[selectedId] ?? \"\"\n  /** `null` = \"the key already stored\", which this component cannot read. */\n  const activeKey: string | null = stored ? null : draftKey\n\n  /**\n   * A verdict is about one exact (slot, key) pair. Change either and the panel\n   * on screen is answering a question nobody asked any more — so the binding,\n   * not a timer, is what expires it.\n   */\n  const binding = `${activeProviderId} ${activeKey ?? \"«stored»\"}`\n  const [prevBinding, setPrevBinding] = React.useState(binding)\n  if (prevBinding !== binding) {\n    setPrevBinding(binding)\n    if (internalTest.phase !== \"idle\") setInternalTest(IDLE)\n    if (actionError !== null) setActionError(null)\n  }\n\n  const test = testProp ?? internalTest\n  const testing = test.phase === \"testing\"\n  const verdict = test.phase === \"done\" ? test.result : null\n  const verified = verdict !== null && verdict.ok\n\n  const check = React.useMemo<ByokKeyCheck>(\n    () => (entryMode ? checkByokKey(draftKey, provider, providers) : { ok: true }),\n    [entryMode, draftKey, provider, providers],\n  )\n  const showProblem = check.problem !== undefined && check.problem !== \"empty\" && (!interacted || committed)\n\n  const busy = disabled || testing || saving || removing\n  const canTest = !!onTest && !busy && (entryMode ? check.ok : true)\n  const canSave = !!onSave && !busy && entryMode && check.ok && (!requireTestBeforeSave || verified)\n  const canCancelTest = testing && (testProp === undefined || !!onTestCancel)\n  const gateBlocked = !!onSave && entryMode && check.ok && requireTestBeforeSave && !verified\n  const describedBy =\n    [showProblem ? problemId : null, provider.note ? noteId : null].filter(Boolean).join(\" \") || undefined\n\n  const abortInFlight = React.useCallback(() => {\n    const current = inFlightRef.current\n    if (!current) return\n    inFlightRef.current = null\n    if (current.timer !== null) window.clearTimeout(current.timer)\n    current.controller?.abort()\n  }, [])\n\n  React.useEffect(() => {\n    // Re-armed on mount: StrictMode runs mount → cleanup → mount in dev, and a\n    // flag only ever set to false would freeze the live instance mid-pending.\n    mountedRef.current = true\n    return () => {\n      mountedRef.current = false\n    }\n  }, [])\n\n  // Cancel whatever is in flight when the question changes — and on unmount.\n  React.useEffect(() => () => abortInFlight(), [binding, abortInFlight])\n\n  // Normalization deleted characters under the cursor; put the caret back on\n  // the same character instead of letting the re-render throw it to the end.\n  React.useLayoutEffect(() => {\n    const caret = caretRef.current\n    caretRef.current = null\n    const el = inputRef.current\n    if (caret === null || !el || document.activeElement !== el) return\n    el.setSelectionRange(caret, caret)\n  })\n\n  // The panel that held the keyboard focus is gone (key removed, or Replace\n  // pressed). Hand focus to the field that replaced it rather than dropping\n  // the user on <body>.\n  React.useLayoutEffect(() => {\n    if (!focusKeyRef.current) return\n    const el = inputRef.current\n    if (!el) return\n    focusKeyRef.current = false\n    el.focus()\n  }, [entryMode, connection])\n\n  const selectProvider = (nextId: string, carryKey?: string) => {\n    if (carryKey !== undefined) {\n      // Moving, not copying: the key was never this slot's to begin with.\n      setDrafts(prev => ({ ...prev, [selectedId]: \"\", [nextId]: carryKey }))\n      onKeyChange?.(carryKey)\n    }\n    if (nextId === selectedId) return\n    if (providerIdProp === undefined) setInternalProviderId(nextId)\n    onProviderChange?.(nextId)\n  }\n\n  const handleKeyInput = (event: React.ChangeEvent<HTMLInputElement>) => {\n    const raw = event.target.value\n    const caret = event.target.selectionStart ?? raw.length\n    const next = normalizeByokKey(raw)\n    if (next !== raw) {\n      caretRef.current = Math.min(next.length, normalizeByokKey(raw.slice(0, caret)).length)\n      setCleanedFor(next)\n    } else if (cleanedFor !== null) {\n      setCleanedFor(null)\n    }\n    setInteracted(true)\n    setDrafts(prev => ({ ...prev, [selectedId]: next }))\n    onKeyChange?.(next)\n  }\n\n  const runTest = (targetProviderId: string, key: string | null) => {\n    if (!onTest || busy) return\n    abortInFlight()\n    setActionError(null)\n\n    const id = (requestSeqRef.current += 1)\n    const controller = typeof AbortController !== \"undefined\" ? new AbortController() : null\n    const entry: { id: number; controller: AbortController | null; timer: number | null } = {\n      id,\n      controller,\n      timer: null,\n    }\n    inFlightRef.current = entry\n    const startedAt = elapsed()\n    setInternalTest({ phase: \"testing\" })\n\n    const settle = (result: ByokTestResult) => {\n      // The id guard is the only race gate needed: a superseded, cancelled or\n      // already-timed-out request finds a different (or no) entry here and\n      // drops its answer silently — including the rejection an abort causes.\n      if (!mountedRef.current || inFlightRef.current?.id !== id) return\n      if (entry.timer !== null) window.clearTimeout(entry.timer)\n      inFlightRef.current = null\n      const measured = Math.round(elapsed() - startedAt)\n      setInternalTest({\n        phase: \"done\",\n        result: result.ok ? { ...result, latencyMs: result.latencyMs ?? measured } : result,\n      })\n    }\n\n    if (testTimeout > 0) {\n      entry.timer = window.setTimeout(() => {\n        controller?.abort()\n        settle({ ok: false, code: \"timeout\", reason: `No answer in ${Math.round(testTimeout / 1000)}s` })\n      }, testTimeout)\n    }\n\n    try {\n      const outcome = onTest({ providerId: targetProviderId, key, signal: controller?.signal })\n      if (outcome instanceof Promise) {\n        outcome.then(settle, error => settle({ ok: false, code: \"unknown\", reason: errorText(error) }))\n      } else {\n        settle(outcome)\n      }\n    } catch (error) {\n      settle({ ok: false, code: \"unknown\", reason: errorText(error) })\n    }\n  }\n\n  const cancelTest = () => {\n    abortInFlight()\n    setInternalTest(IDLE)\n    onTestCancel?.()\n  }\n\n  const requestSave = () => {\n    if (!onSave || !canSave || saveLockRef.current) return\n    saveLockRef.current = true\n    setSaving(true)\n    setActionError(null)\n\n    const key = draftKey\n    const targetId = selectedId\n\n    const finish = (error?: string) => {\n      if (!mountedRef.current) return\n      saveLockRef.current = false\n      setSaving(false)\n      if (error !== undefined) {\n        // Nothing is cleared on failure: every character the user pasted is\n        // still in the field, ready to be retried.\n        setActionError(error)\n        return\n      }\n      // The plaintext is dropped the moment the save lands — from here on this\n      // component holds a hint, never a secret. A controlled consumer has to\n      // hand back `connection` in the same update.\n      setDrafts(prev => ({ ...prev, [targetId]: \"\" }))\n      setInternalTest(IDLE)\n      setRevealed(false)\n      setInteracted(false)\n      setCommitted(false)\n      setCleanedFor(null)\n      setReplacing(false)\n      if (connectionProp === undefined) {\n        setInternalConnection({ providerId: targetId, keyHint: byokKeyHint(key), addedAtLabel: \"just now\" })\n      }\n    }\n\n    // try/catch as well as the rejection path: a handler that throws\n    // synchronously (a storage quota, a permission check) would otherwise escape\n    // and leave the card pinned at \"Saving…\" with the lock still held.\n    try {\n      const outcome = onSave({ providerId: targetId, key })\n      if (outcome instanceof Promise) {\n        outcome.then(\n          () => finish(),\n          error => finish(errorText(error)),\n        )\n      } else {\n        finish()\n      }\n    } catch (error) {\n      finish(errorText(error))\n    }\n  }\n\n  const confirmRemove = () => {\n    if (!onRemove || !stored || removeLockRef.current) return\n    removeLockRef.current = true\n    setRemoving(true)\n\n    const finish = (error?: string) => {\n      if (!mountedRef.current) return\n      removeLockRef.current = false\n      setRemoving(false)\n      setRemoveOpen(false)\n      if (error !== undefined) {\n        setActionError(error)\n        return\n      }\n      setActionError(null)\n      setInternalTest(IDLE)\n      setDrafts({})\n      setReplacing(false)\n      setRevealed(false)\n      setInteracted(false)\n      setCommitted(false)\n      // The dialog's trigger unmounts together with the connection, so\n      // nominate where focus goes before Radix tries to return it there.\n      focusKeyRef.current = true\n      suppressReturnFocusRef.current = true\n      if (connectionProp === undefined) setInternalConnection(null)\n    }\n\n    try {\n      const outcome = onRemove(stored.providerId)\n      if (outcome instanceof Promise) {\n        outcome.then(\n          () => finish(),\n          error => finish(errorText(error)),\n        )\n      } else {\n        finish()\n      }\n    } catch (error) {\n      finish(errorText(error))\n    }\n  }\n\n  const submit = () => {\n    if (!entryMode || busy) return\n    setCommitted(true)\n    if (!check.ok) {\n      inputRef.current?.focus()\n      return\n    }\n    // Enter tests while the key is unproven and saves once it is — the same\n    // order the two buttons sit in.\n    if (canSave) requestSave()\n    else if (canTest) runTest(selectedId, draftKey)\n  }\n\n  const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {\n    event.preventDefault()\n    submit()\n  }\n\n  const StorageIcon = SCOPE_ICON[storageScope]\n  const disclaimer = (\n    <p className=\"flex items-start gap-2 rounded-lg bg-muted/50 px-3 py-2 text-xs text-muted-foreground\">\n      <StorageIcon aria-hidden=\"true\" className=\"mt-px size-3.5 shrink-0\" />\n      <span className=\"min-w-0\">{storageNote ?? SCOPE_COPY[storageScope]}</span>\n    </p>\n  )\n\n  const failure = actionError ? (\n    <p className=\"flex items-start gap-2 text-xs text-destructive\" role=\"alert\">\n      <TriangleAlert aria-hidden=\"true\" className=\"mt-px size-3 shrink-0\" />\n      <span className=\"min-w-0\">{actionError}</span>\n    </p>\n  ) : null\n\n  let testPanel: React.ReactNode = null\n  if (testing) {\n    testPanel = (\n      <div className=\"flex items-center gap-2 rounded-lg border border-dashed px-3 py-2 text-xs text-muted-foreground\">\n        <LoaderCircle aria-hidden=\"true\" className=\"size-3.5 shrink-0 animate-spin motion-reduce:animate-none\" />\n        <span className=\"min-w-0 flex-1\">Asking {provider.name} whether this key works…</span>\n        {canCancelTest ? (\n          <Button className=\"-my-1 shrink-0\" onClick={cancelTest} size=\"xs\" type=\"button\" variant=\"ghost\">\n            Cancel\n          </Button>\n        ) : null}\n      </div>\n    )\n  } else if (verdict !== null) {\n    testPanel = verdict.ok ? (\n      <div className=\"flex flex-wrap items-center gap-x-2 gap-y-1 rounded-lg border px-3 py-2 text-xs\">\n        <CircleCheck aria-hidden=\"true\" className=\"size-3.5 shrink-0\" style={{ color: \"var(--chart-2)\" }} />\n        <span className=\"font-medium\">Key works</span>\n        {verdict.latencyMs !== undefined ? (\n          <span className=\"text-muted-foreground tabular-nums\">\n            {verdict.latencyMs} ms{verdict.latencyMs >= SLOW_MS ? \" · slow first call\" : \"\"}\n          </span>\n        ) : null}\n        {verdict.detail ? <span className=\"min-w-0 text-muted-foreground\">· {verdict.detail}</span> : null}\n      </div>\n    ) : (\n      <div\n        className=\"flex flex-col gap-1.5 rounded-lg border border-destructive/40 bg-destructive/10 px-3 py-2 text-xs text-destructive\"\n        role=\"alert\"\n      >\n        <span className=\"flex items-start gap-2 font-medium\">\n          <TriangleAlert aria-hidden=\"true\" className=\"mt-px size-3.5 shrink-0\" />\n          <span className=\"min-w-0\">{verdict.reason}</span>\n        </span>\n        {verdict.hint ?? FAILURE_HINT[verdict.code ?? \"unknown\"] ? (\n          <span className=\"pl-5.5\">{verdict.hint ?? FAILURE_HINT[verdict.code ?? \"unknown\"]}</span>\n        ) : null}\n        {onTest ? (\n          <span className=\"pl-5.5\">\n            <Button\n              disabled={!canTest}\n              onClick={() => runTest(activeProviderId, activeKey)}\n              size=\"xs\"\n              type=\"button\"\n              variant=\"outline\"\n            >\n              <RefreshCw aria-hidden=\"true\" />\n              Test again\n            </Button>\n          </span>\n        ) : null}\n      </div>\n    )\n  }\n\n  const liveMessage = testing\n    ? `Testing the ${provider.name} key.`\n    : saving\n      ? \"Saving the key.\"\n      : removing\n        ? \"Removing the key.\"\n        : actionError\n          ? actionError\n          : verdict === null\n            ? \"\"\n            : verdict.ok\n              ? `The ${provider.name} key works${verdict.latencyMs !== undefined ? `, answered in ${verdict.latencyMs} milliseconds` : \"\"}.`\n              : `The ${provider.name} key was not accepted. ${verdict.reason}`\n\n  return (\n    <form\n      aria-busy={testing || saving || removing || undefined}\n      aria-labelledby={titleId}\n      className={cn(\n        \"flex w-full min-w-0 flex-col overflow-hidden rounded-xl border bg-card text-card-foreground shadow-xs\",\n        disabled && \"opacity-60\",\n        className,\n      )}\n      onSubmit={handleSubmit}\n      ref={ref}\n      {...props}\n    >\n      <div className=\"flex items-start gap-2.5 border-b px-4 py-3\">\n        <span\n          aria-hidden=\"true\"\n          className=\"mt-0.5 inline-flex size-7 shrink-0 items-center justify-center rounded-lg bg-muted text-muted-foreground\"\n        >\n          <KeyRound className=\"size-3.5\" />\n        </span>\n        <div className=\"flex min-w-0 flex-col gap-0.5\">\n          <h3 className=\"text-sm font-medium\" id={titleId}>\n            {title}\n          </h3>\n          {description ? <p className=\"text-xs text-muted-foreground\">{description}</p> : null}\n        </div>\n      </div>\n\n      {!hasProviders ? (\n        <p className=\"px-4 py-6 text-xs text-muted-foreground\">\n          No providers are configured for this workspace, so there is no slot to put a key in.\n        </p>\n      ) : stored ? (\n        <div className=\"flex flex-col gap-4 px-4 py-4\">\n          <div className=\"flex flex-col gap-1\">\n            <div className=\"flex flex-wrap items-center gap-x-2 gap-y-1.5\">\n              <span className=\"text-sm font-medium\">{provider.name}</span>\n              <Badge variant=\"secondary\">\n                <CircleCheck aria-hidden=\"true\" style={{ color: \"var(--chart-2)\" }} />\n                Key on file\n              </Badge>\n              <code className=\"ml-auto rounded-md bg-muted px-1.5 py-0.5 font-mono text-xs text-muted-foreground\">\n                {stored.keyHint}\n              </code>\n            </div>\n            {stored.addedAtLabel || stored.lastVerifiedLabel ? (\n              <p className=\"flex flex-wrap gap-x-3 gap-y-0.5 text-xs text-muted-foreground\">\n                {stored.addedAtLabel ? <span>Added {stored.addedAtLabel}</span> : null}\n                {stored.lastVerifiedLabel ? <span>Last verified {stored.lastVerifiedLabel}</span> : null}\n              </p>\n            ) : null}\n          </div>\n\n          {testPanel}\n          {disclaimer}\n          {failure}\n        </div>\n      ) : (\n        <div className=\"flex flex-col gap-4 px-4 py-4\">\n          <div className=\"flex flex-col gap-2\">\n            <span className=\"text-xs font-medium text-muted-foreground\" id={providerLabelId}>\n              Provider\n            </span>\n            <ProviderPicker\n              disabled={disabled || testing || saving}\n              labelledBy={providerLabelId}\n              onSelect={id => selectProvider(id)}\n              providers={providers}\n              value={selectedId}\n            />\n          </div>\n\n          <div className=\"flex min-w-0 flex-col gap-1.5\">\n            <div className=\"flex flex-wrap items-baseline gap-x-2 gap-y-1\">\n              <label className=\"text-sm font-medium\" htmlFor={keyId}>\n                {provider.name} API key\n              </label>\n              {provider.consoleUrl ? (\n                <a\n                  className=\"ml-auto inline-flex items-center gap-1 rounded-sm text-xs text-muted-foreground underline-offset-2 outline-none hover:text-foreground hover:underline focus-visible:ring-3 focus-visible:ring-ring/50\"\n                  href={provider.consoleUrl}\n                  rel=\"noopener noreferrer\"\n                  target=\"_blank\"\n                >\n                  {provider.consoleLabel ?? \"Get a key\"}\n                  <ExternalLink aria-hidden=\"true\" className=\"size-3\" />\n                </a>\n              ) : null}\n            </div>\n\n            <div\n              className={cn(\n                \"flex h-9 w-full min-w-0 items-center gap-1 rounded-lg border border-input bg-transparent px-3 shadow-xs transition-colors\",\n                \"focus-within:border-ring focus-within:ring-3 focus-within:ring-ring/50\",\n                showProblem && \"border-destructive/60\",\n                disabled && \"cursor-not-allowed opacity-60\",\n              )}\n            >\n              <input\n                aria-describedby={describedBy}\n                aria-invalid={showProblem || undefined}\n                autoCapitalize=\"off\"\n                autoComplete=\"off\"\n                autoCorrect=\"off\"\n                className=\"min-w-0 flex-1 bg-transparent font-mono text-sm outline-none placeholder:font-sans placeholder:text-muted-foreground disabled:cursor-not-allowed\"\n                disabled={disabled}\n                id={keyId}\n                onBlur={() => setCommitted(true)}\n                onChange={handleKeyInput}\n                // Save is this form's default button and it stays disabled\n                // until the key is proven — and a disabled default button\n                // suppresses implicit submission entirely, which would leave\n                // Enter dead exactly when it is meant to run the test.\n                onKeyDown={event => {\n                  if (event.key !== \"Enter\") return\n                  event.preventDefault()\n                  submit()\n                }}\n                placeholder={provider.placeholder ?? \"Paste your key\"}\n                // readOnly while the save is in flight, never `disabled`: Enter\n                // in this field is what starts the save, and `disabled` would\n                // blur the control the user is standing on — leaving them on\n                // <body> when a rejected save hands the field back with their\n                // key still in it. The busy guard in `submit` does the blocking.\n                readOnly={saving}\n                ref={inputRef}\n                spellCheck={false}\n                // A password field for an editable secret: shoulder-surfing is\n                // the threat here, not the length of the string.\n                type={revealed ? \"text\" : \"password\"}\n                value={draftKey}\n              />\n              <button\n                aria-label={revealed ? \"Hide the key\" : \"Show the key\"}\n                aria-pressed={revealed}\n                className=\"inline-flex size-7 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors outline-none hover:bg-muted hover:text-foreground focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-40\"\n                disabled={disabled || draftKey.length === 0}\n                onClick={() => setRevealed(current => !current)}\n                type=\"button\"\n              >\n                {revealed ? (\n                  <EyeOff aria-hidden=\"true\" className=\"size-4\" />\n                ) : (\n                  <Eye aria-hidden=\"true\" className=\"size-4\" />\n                )}\n              </button>\n            </div>\n\n            {showProblem ? (\n              <p className=\"flex flex-wrap items-center gap-x-1.5 gap-y-1 text-xs text-destructive\" id={problemId}>\n                <TriangleAlert aria-hidden=\"true\" className=\"size-3 shrink-0\" />\n                <span className=\"min-w-0\">{check.message}</span>\n                {check.suggestion ? (\n                  <Button\n                    className=\"-my-1\"\n                    disabled={busy}\n                    onClick={() => {\n                      const suggested = check.suggestion\n                      if (!suggested) return\n                      selectProvider(suggested.id, draftKey)\n                      setRevealed(false)\n                    }}\n                    size=\"xs\"\n                    type=\"button\"\n                    variant=\"outline\"\n                  >\n                    Move it to {check.suggestion.name}\n                    <ArrowRight aria-hidden=\"true\" />\n                  </Button>\n                ) : null}\n              </p>\n            ) : null}\n\n            {cleanedFor !== null && cleanedFor === draftKey ? (\n              <p className=\"text-xs text-muted-foreground\">\n                Cleaned that paste — quotes, line breaks and a leading “Bearer” are not part of the key.\n              </p>\n            ) : null}\n\n            {provider.note ? (\n              <p className=\"text-xs text-muted-foreground\" id={noteId}>\n                {provider.note}\n              </p>\n            ) : null}\n          </div>\n\n          {testPanel}\n          {disclaimer}\n          {failure}\n        </div>\n      )}\n\n      {hasProviders && (stored ? onTest || onRemove || onSave : onTest || onSave) ? (\n        <div className=\"flex flex-wrap items-center gap-2 border-t bg-muted/40 px-4 py-3\">\n          {stored ? (\n            <>\n              {onTest ? (\n                <Button\n                  disabled={!canTest}\n                  onClick={() => runTest(stored.providerId, null)}\n                  size=\"sm\"\n                  type=\"button\"\n                  variant=\"outline\"\n                >\n                  {testing ? (\n                    <LoaderCircle aria-hidden=\"true\" className=\"animate-spin motion-reduce:animate-none\" />\n                  ) : (\n                    <RefreshCw aria-hidden=\"true\" />\n                  )}\n                  {testing ? \"Testing…\" : \"Test connection\"}\n                </Button>\n              ) : null}\n              {/* Replacing is only an affordance if there is somewhere to save\n                  the replacement to. */}\n              {onSave ? (\n                <Button\n                  disabled={busy}\n                  onClick={() => {\n                    selectProvider(stored.providerId)\n                    setInternalTest(IDLE)\n                    setInteracted(false)\n                    setCommitted(false)\n                    focusKeyRef.current = true\n                    setReplacing(true)\n                  }}\n                  size=\"sm\"\n                  type=\"button\"\n                  variant=\"outline\"\n                >\n                  Replace key\n                </Button>\n              ) : null}\n              {onRemove ? (\n                <AlertDialog\n                  onOpenChange={next => {\n                    if (!removing) setRemoveOpen(next)\n                  }}\n                  open={removeOpen}\n                >\n                  <AlertDialogTrigger asChild>\n                    <Button className=\"ml-auto\" disabled={busy} size=\"sm\" type=\"button\" variant=\"destructive\">\n                      <Trash2 aria-hidden=\"true\" />\n                      Remove key\n                    </Button>\n                  </AlertDialogTrigger>\n                  <AlertDialogContent\n                    onCloseAutoFocus={event => {\n                      // The layout effect above is handing focus to the key\n                      // field; Radix would send it back to a trigger that has\n                      // just unmounted with the connection.\n                      if (!suppressReturnFocusRef.current) return\n                      suppressReturnFocusRef.current = false\n                      event.preventDefault()\n                    }}\n                  >\n                    <AlertDialogHeader>\n                      <AlertDialogTitle>Remove the {provider.name} key?</AlertDialogTitle>\n                      <AlertDialogDescription>\n                        {stored.keyHint} is deleted {SCOPE_WHERE[storageScope]}. Anything running on it stops working\n                        straight away, and you will have to paste a key again to bring it back. This does not revoke the\n                        key at {provider.name} — do that in their console if it leaked.\n                      </AlertDialogDescription>\n                    </AlertDialogHeader>\n                    <AlertDialogFooter>\n                      <AlertDialogCancel disabled={removing}>Keep it</AlertDialogCancel>\n                      {/* A plain Button, not AlertDialogAction: closing is this\n                          component's job, so the dialog can stay open and\n                          pending while onRemove is in flight. */}\n                      <Button disabled={removing} onClick={confirmRemove} type=\"button\" variant=\"destructive\">\n                        {removing ? (\n                          <LoaderCircle aria-hidden=\"true\" className=\"animate-spin motion-reduce:animate-none\" />\n                        ) : (\n                          <Trash2 aria-hidden=\"true\" />\n                        )}\n                        {removing ? \"Removing…\" : \"Remove key\"}\n                      </Button>\n                    </AlertDialogFooter>\n                  </AlertDialogContent>\n                </AlertDialog>\n              ) : null}\n            </>\n          ) : (\n            <>\n              {onTest ? (\n                <Button\n                  disabled={!canTest}\n                  onClick={() => runTest(selectedId, draftKey)}\n                  size=\"sm\"\n                  type=\"button\"\n                  variant=\"outline\"\n                >\n                  {testing ? (\n                    <LoaderCircle aria-hidden=\"true\" className=\"animate-spin motion-reduce:animate-none\" />\n                  ) : verdict !== null ? (\n                    <RefreshCw aria-hidden=\"true\" />\n                  ) : (\n                    <Plug aria-hidden=\"true\" />\n                  )}\n                  {testing ? \"Testing…\" : verdict !== null ? \"Test again\" : \"Test connection\"}\n                </Button>\n              ) : null}\n              {onSave ? (\n                <Button aria-describedby={gateBlocked ? gateId : undefined} disabled={!canSave} size=\"sm\" type=\"submit\">\n                  {saving ? (\n                    <LoaderCircle aria-hidden=\"true\" className=\"animate-spin motion-reduce:animate-none\" />\n                  ) : null}\n                  {saving ? \"Saving…\" : \"Save key\"}\n                </Button>\n              ) : null}\n              {connection ? (\n                <Button\n                  disabled={busy}\n                  onClick={() => {\n                    setReplacing(false)\n                    setInternalTest(IDLE)\n                    setDrafts(prev => ({ ...prev, [selectedId]: \"\" }))\n                    setRevealed(false)\n                  }}\n                  size=\"sm\"\n                  type=\"button\"\n                  variant=\"ghost\"\n                >\n                  Cancel\n                </Button>\n              ) : null}\n              {gateBlocked ? (\n                <span className=\"ml-auto text-xs text-muted-foreground\" id={gateId}>\n                  Test the key before saving it.\n                </span>\n              ) : null}\n            </>\n          )}\n        </div>\n      ) : null}\n\n      {/* Persistent live region — never conditionally unmounted, so every\n          transition actually gets announced. */}\n      <div aria-live=\"polite\" className=\"sr-only\" role=\"status\">\n        {liveMessage}\n      </div>\n    </form>\n  )\n})\n\nByokSetup.displayName = \"ByokSetup\"\n\nexport default ByokSetup\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}