{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "key-value-editor",
  "title": "Key Value Editor",
  "description": "Editable key and value rows for env vars and headers, with .env paste, duplicate flags and masked values.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "button",
    "input",
    "@drift-ui/password-input",
    "@drift-ui/use-controllable-state"
  ],
  "files": [
    {
      "path": "registry/default/ui/key-value-editor.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { PlusIcon, XIcon } from \"lucide-react\"\n\nimport { useControllableState } from \"@/registry/default/hooks/use-controllable-state\"\nimport {\n  createRow,\n  findDuplicateKeys,\n  pasteRows,\n  type KeyValueRow,\n} from \"@/registry/default/lib/key-value-editor.ts\"\nimport { cn } from \"@/registry/default/lib/utils\"\nimport { Button } from \"@/registry/default/ui/button\"\nimport { Input } from \"@/registry/default/ui/input\"\nimport { PasswordInput } from \"@/registry/default/ui/password-input\"\n\nconst EMPTY_ROWS: KeyValueRow[] = []\n\n// Never equal to a row id, so any id is safe to use.\nconst ADD_BUTTON = Symbol(\"add button\")\ntype FocusTarget = string | typeof ADD_BUTTON\n\nfunction KeyValueEditor({\n  value,\n  defaultValue = EMPTY_ROWS,\n  onValueChange,\n  secretValues = false,\n  keyLabel = \"Key\",\n  valueLabel = \"Value\",\n  keyPlaceholder = \"KEY\",\n  valuePlaceholder = \"Value\",\n  addLabel = \"Add row\",\n  disabled = false,\n  className,\n  ...props\n}: Omit<React.ComponentProps<\"div\">, \"defaultValue\"> & {\n  value?: KeyValueRow[]\n  defaultValue?: KeyValueRow[]\n  onValueChange?: (rows: KeyValueRow[]) => void\n  secretValues?: boolean\n  keyLabel?: string\n  valueLabel?: string\n  keyPlaceholder?: string\n  valuePlaceholder?: string\n  addLabel?: string\n  disabled?: boolean\n}) {\n  const { value: rows, setValue: setRows } = useControllableState({\n    value,\n    defaultValue,\n    onChange: onValueChange,\n  })\n  const rootRef = React.useRef<HTMLDivElement>(null)\n  // Where focus goes after the next render: a row's key input, or the add button.\n  const pendingFocus = React.useRef<FocusTarget | null>(null)\n  const baseId = React.useId()\n  const duplicates = findDuplicateKeys(rows)\n\n  function focusTarget(target: FocusTarget) {\n    const root = rootRef.current\n    if (!root) return\n    // Compared, not interpolated into a selector, so any id works.\n    const container =\n      target === ADD_BUTTON\n        ? root\n        : [\n            ...root.querySelectorAll<HTMLElement>(\n              '[data-slot=\"key-value-editor-row\"]'\n            ),\n          ].find((row) => row.dataset.rowId === target)\n    container\n      ?.querySelector<HTMLElement>(\n        target === ADD_BUTTON\n          ? '[data-slot=\"key-value-editor-add\"]'\n          : '[data-slot=\"key-value-editor-key\"]'\n      )\n      ?.focus()\n  }\n\n  React.useLayoutEffect(() => {\n    const target = pendingFocus.current\n    if (target === null) return\n    pendingFocus.current = null\n    focusTarget(target)\n  })\n\n  function update(id: string, patch: Partial<KeyValueRow>) {\n    setRows(rows.map((row) => (row.id === id ? { ...row, ...patch } : row)))\n  }\n\n  function addRow() {\n    const row = createRow()\n    pendingFocus.current = row.id\n    setRows([...rows, row])\n  }\n\n  function removeRow(index: number) {\n    const neighbour = rows[index + 1] ?? rows[index - 1]\n    pendingFocus.current = neighbour ? neighbour.id : ADD_BUTTON\n    setRows(rows.filter((_, rowIndex) => rowIndex !== index))\n  }\n\n  function handleKeyDown(\n    event: React.KeyboardEvent<HTMLInputElement>,\n    index: number\n  ) {\n    if (event.key !== \"Enter\" || event.nativeEvent.isComposing) return\n    event.preventDefault()\n    if (index === rows.length - 1) addRow()\n    else focusTarget(rows[index + 1].id)\n  }\n\n  function handlePaste(\n    event: React.ClipboardEvent<HTMLInputElement>,\n    index: number\n  ) {\n    const next = pasteRows(rows, index, event.clipboardData.getData(\"text\"))\n    if (!next) return\n    event.preventDefault()\n    pendingFocus.current = next.at(-1)?.id ?? null\n    setRows(next)\n  }\n\n  return (\n    <div\n      ref={rootRef}\n      role=\"group\"\n      data-slot=\"key-value-editor\"\n      className={cn(\"grid gap-2\", className)}\n      {...props}\n    >\n      {rows.length > 0 && (\n        <div\n          aria-hidden\n          className=\"grid grid-cols-[1fr_1fr_2.25rem] gap-2 text-xs font-medium text-muted-foreground\"\n        >\n          <span>{keyLabel}</span>\n          <span>{valueLabel}</span>\n        </div>\n      )}\n      {rows.map((row, index) => {\n        const number = index + 1\n        const isDuplicate = duplicates.has(row.key.trim())\n        const errorId = `${baseId}-${row.id}-error`\n        const ValueInput = secretValues ? PasswordInput : Input\n        return (\n          <div\n            key={row.id}\n            data-row-id={row.id}\n            data-slot=\"key-value-editor-row\"\n            className=\"grid grid-cols-[1fr_1fr_2.25rem] gap-2\"\n          >\n            <Input\n              data-slot=\"key-value-editor-key\"\n              aria-label={`${keyLabel} ${number}`}\n              aria-invalid={isDuplicate || undefined}\n              aria-describedby={isDuplicate ? errorId : undefined}\n              placeholder={keyPlaceholder}\n              value={row.key}\n              disabled={disabled}\n              spellCheck={false}\n              autoComplete=\"off\"\n              className=\"font-mono\"\n              onChange={(event) => update(row.id, { key: event.target.value })}\n              onKeyDown={(event) => handleKeyDown(event, index)}\n              onPaste={(event) => handlePaste(event, index)}\n            />\n            <ValueInput\n              data-slot=\"key-value-editor-value\"\n              aria-label={`${valueLabel} ${number}`}\n              placeholder={valuePlaceholder}\n              value={row.value}\n              disabled={disabled}\n              spellCheck={false}\n              autoComplete=\"off\"\n              className=\"font-mono\"\n              onChange={(event) =>\n                update(row.id, { value: event.target.value })\n              }\n              onKeyDown={(event) => handleKeyDown(event, index)}\n            />\n            <Button\n              type=\"button\"\n              variant=\"ghost\"\n              size=\"icon\"\n              aria-label={`Remove row ${number}`}\n              disabled={disabled}\n              onClick={() => removeRow(index)}\n            >\n              <XIcon />\n            </Button>\n            {isDuplicate && (\n              <p id={errorId} className=\"col-span-3 text-xs text-destructive\">\n                Duplicate key\n              </p>\n            )}\n          </div>\n        )\n      })}\n      <Button\n        type=\"button\"\n        variant=\"outline\"\n        size=\"sm\"\n        data-slot=\"key-value-editor-add\"\n        disabled={disabled}\n        onClick={addRow}\n        className=\"justify-self-start\"\n      >\n        <PlusIcon />\n        {addLabel}\n      </Button>\n    </div>\n  )\n}\n\nexport { KeyValueEditor, type KeyValueRow }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/default/lib/key-value-editor.ts",
      "content": "export type KeyValueRow = { id: string; key: string; value: string }\n\nexport function createRowId() {\n  return Math.random().toString(36).slice(2, 10)\n}\n\nexport function createRow(key = \"\", value = \"\"): KeyValueRow {\n  return { id: createRowId(), key, value }\n}\n\n// One KEY=value line in .env style, or null for blank lines, comments and\n// lines without \"=\". Multi-line values are not supported.\nexport function parseEnvLine(line: string) {\n  const trimmed = line.trim()\n  if (trimmed === \"\" || trimmed.startsWith(\"#\")) return null\n  const body = trimmed.replace(/^export\\s+/, \"\")\n  const equals = body.indexOf(\"=\")\n  if (equals <= 0) return null\n\n  const key = body.slice(0, equals).trim()\n  const rest = body.slice(equals + 1).trim()\n  const value = readQuoted(rest) ?? rest.replace(/\\s+#.*$/, \"\")\n  return key ? { key, value } : null\n}\n\n// A quoted value up to its closing quote. Double quotes allow \\\" and \\\\\n// escapes; single quotes are literal. An unquoted value ends at \" #\".\nfunction readQuoted(text: string) {\n  const quote = text[0]\n  if (quote !== '\"' && quote !== \"'\") return null\n  let value = \"\"\n  for (let index = 1; index < text.length; index++) {\n    const character = text[index]\n    if (character === quote) return value\n    const escaped = text[index + 1]\n    if (\n      quote === '\"' &&\n      character === \"\\\\\" &&\n      (escaped === '\"' || escaped === \"\\\\\")\n    ) {\n      value += text[++index]\n      continue\n    }\n    value += character\n  }\n  return null\n}\n\nexport function parseEnv(text: string) {\n  return text\n    .split(/\\r?\\n/)\n    .map(parseEnvLine)\n    .filter((entry) => entry !== null)\n}\n\n// Text pasted into a key field becomes rows when it looks like KEY=value.\nexport function pasteRows(rows: KeyValueRow[], index: number, text: string) {\n  if (!text.includes(\"=\")) return null\n  const entries = parseEnv(text)\n  if (entries.length === 0) return null\n\n  const pasted = entries.map((entry) => createRow(entry.key, entry.value))\n  const current = rows[index]\n  const replacesCurrent = current.key === \"\" && current.value === \"\"\n  return [\n    ...rows.slice(0, index + (replacesCurrent ? 0 : 1)),\n    ...pasted,\n    ...rows.slice(index + 1),\n  ]\n}\n\n// Keys used by more than one row, compared after trimming. Empty keys are\n// never duplicates.\nexport function findDuplicateKeys(rows: KeyValueRow[]) {\n  const seen = new Set<string>()\n  const duplicates = new Set<string>()\n  for (const row of rows) {\n    const key = row.key.trim()\n    if (key === \"\") continue\n    if (seen.has(key)) duplicates.add(key)\n    seen.add(key)\n  }\n  return duplicates\n}\n",
      "type": "registry:lib"
    }
  ],
  "type": "registry:ui"
}