{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "env-editor",
  "title": "Env Editor",
  "description": "Environment variable editor with key-value grid, masked values, and import/export",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/blocks/devtools/env-editor/components/elements/env-editor.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { Download, Eye, EyeOff, Plus, Trash2, Upload } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\n\ninterface EnvVariable {\n  key: string;\n  value: string;\n}\n\ninterface EnvEditorProps {\n  value?: EnvVariable[];\n  onChange?: (variables: EnvVariable[]) => void;\n  readOnly?: boolean;\n  masked?: boolean;\n  className?: string;\n}\n\nfunction parseEnvString(content: string): EnvVariable[] {\n  const lines = content.split(\"\\n\");\n  const variables: EnvVariable[] = [];\n\n  for (const line of lines) {\n    const trimmed = line.trim();\n    if (!trimmed || trimmed.startsWith(\"#\")) continue;\n\n    const eqIndex = trimmed.indexOf(\"=\");\n    if (eqIndex === -1) continue;\n\n    const key = trimmed.slice(0, eqIndex).trim();\n    let value = trimmed.slice(eqIndex + 1).trim();\n\n    if (\n      (value.startsWith('\"') && value.endsWith('\"')) ||\n      (value.startsWith(\"'\") && value.endsWith(\"'\"))\n    ) {\n      value = value.slice(1, -1);\n    }\n\n    variables.push({ key, value });\n  }\n\n  return variables;\n}\n\nfunction toEnvString(variables: EnvVariable[]): string {\n  return variables\n    .map(({ key, value }) => {\n      const needsQuotes =\n        value.includes(\" \") || value.includes(\"=\") || value.includes(\"#\");\n      return `${key}=${needsQuotes ? `\"${value}\"` : value}`;\n    })\n    .join(\"\\n\");\n}\n\nexport function EnvEditor({\n  value = [],\n  onChange,\n  readOnly = false,\n  masked: defaultMasked = true,\n  className,\n}: EnvEditorProps) {\n  const [variables, setVariables] = React.useState<EnvVariable[]>(value);\n  const [maskedKeys, setMaskedKeys] = React.useState<Set<string>>(\n    () => new Set(value.map((v) => v.key)),\n  );\n  const fileInputRef = React.useRef<HTMLInputElement>(null);\n\n  React.useEffect(() => {\n    setVariables(value);\n    if (defaultMasked) {\n      setMaskedKeys(new Set(value.map((v) => v.key)));\n    }\n  }, [value, defaultMasked]);\n\n  const updateVariables = React.useCallback(\n    (newVars: EnvVariable[]) => {\n      setVariables(newVars);\n      onChange?.(newVars);\n    },\n    [onChange],\n  );\n\n  const handleAdd = React.useCallback(() => {\n    const newVar = { key: \"\", value: \"\" };\n    setVariables((prev) => {\n      const updated = [...prev, newVar];\n      onChange?.(updated);\n      return updated;\n    });\n  }, [onChange]);\n\n  const handleRemove = React.useCallback(\n    (index: number) => {\n      setVariables((prev) => {\n        const updated = prev.filter((_, i) => i !== index);\n        onChange?.(updated);\n        return updated;\n      });\n    },\n    [onChange],\n  );\n\n  const handleChange = React.useCallback(\n    (index: number, field: \"key\" | \"value\", newValue: string) => {\n      setVariables((prev) => {\n        const updated = prev.map((v, i) => {\n          if (i === index) {\n            return { ...v, [field]: newValue };\n          }\n          return v;\n        });\n        onChange?.(updated);\n        return updated;\n      });\n    },\n    [onChange],\n  );\n\n  const toggleMask = React.useCallback((key: string) => {\n    setMaskedKeys((prev) => {\n      const next = new Set(prev);\n      if (next.has(key)) {\n        next.delete(key);\n      } else {\n        next.add(key);\n      }\n      return next;\n    });\n  }, []);\n\n  const handleExport = React.useCallback(() => {\n    const content = toEnvString(variables);\n    const blob = new Blob([content], { type: \"text/plain\" });\n    const url = URL.createObjectURL(blob);\n    const a = document.createElement(\"a\");\n    a.href = url;\n    a.download = \".env\";\n    a.click();\n    URL.revokeObjectURL(url);\n  }, [variables]);\n\n  const handleImport = React.useCallback(\n    (e: React.ChangeEvent<HTMLInputElement>) => {\n      const file = e.target.files?.[0];\n      if (!file) return;\n\n      const reader = new FileReader();\n      reader.onload = (evt) => {\n        const content = evt.target?.result as string;\n        const parsed = parseEnvString(content);\n        setVariables(parsed);\n        onChange?.(parsed);\n        if (defaultMasked) {\n          setMaskedKeys(new Set(parsed.map((v) => v.key)));\n        }\n      };\n      reader.readAsText(file);\n\n      if (fileInputRef.current) {\n        fileInputRef.current.value = \"\";\n      }\n    },\n    [onChange, defaultMasked],\n  );\n\n  return (\n    <div\n      data-slot=\"env-editor\"\n      className={cn(\n        \"border border-border rounded-lg overflow-hidden\",\n        className,\n      )}\n    >\n      <div className=\"flex items-center justify-between px-3 py-2 border-b border-border bg-muted/50\">\n        <span className=\"text-sm font-medium\">.env Editor</span>\n        {!readOnly && (\n          <div className=\"flex gap-1\">\n            <button\n              type=\"button\"\n              onClick={() => fileInputRef.current?.click()}\n              className=\"p-1.5 text-muted-foreground hover:text-foreground transition-colors\"\n              aria-label=\"Import .env file\"\n            >\n              <Upload className=\"w-4 h-4\" />\n            </button>\n            <button\n              type=\"button\"\n              onClick={handleExport}\n              className=\"p-1.5 text-muted-foreground hover:text-foreground transition-colors\"\n              aria-label=\"Export .env file\"\n            >\n              <Download className=\"w-4 h-4\" />\n            </button>\n            <input\n              ref={fileInputRef}\n              type=\"file\"\n              accept=\".env,.env.local,.env.development,.env.production\"\n              onChange={handleImport}\n              className=\"hidden\"\n            />\n          </div>\n        )}\n      </div>\n\n      <div className=\"divide-y divide-border\">\n        {variables.map((variable, index) => (\n          <div\n            key={index}\n            className=\"flex items-center gap-2 px-3 py-2\"\n            role=\"row\"\n          >\n            <input\n              type=\"text\"\n              value={variable.key}\n              onChange={(e) => handleChange(index, \"key\", e.target.value)}\n              placeholder=\"KEY\"\n              readOnly={readOnly}\n              aria-label=\"Variable name\"\n              className=\"flex-1 min-w-0 bg-transparent font-mono text-sm text-foreground placeholder:text-muted-foreground focus:outline-none\"\n            />\n            <span className=\"text-muted-foreground\" aria-hidden=\"true\">\n              =\n            </span>\n            <div className=\"flex-[2] flex items-center gap-1\">\n              <input\n                type={maskedKeys.has(variable.key) ? \"password\" : \"text\"}\n                value={variable.value}\n                onChange={(e) => handleChange(index, \"value\", e.target.value)}\n                placeholder=\"value\"\n                readOnly={readOnly}\n                aria-label={`Value for ${variable.key || \"variable\"}`}\n                className=\"flex-1 min-w-0 bg-transparent font-mono text-sm text-foreground placeholder:text-muted-foreground focus:outline-none\"\n              />\n              <button\n                type=\"button\"\n                onClick={() => toggleMask(variable.key)}\n                className=\"p-1 text-muted-foreground hover:text-foreground transition-colors shrink-0\"\n                aria-label={\n                  maskedKeys.has(variable.key) ? \"Show value\" : \"Hide value\"\n                }\n                aria-pressed={maskedKeys.has(variable.key)}\n              >\n                {maskedKeys.has(variable.key) ? (\n                  <Eye className=\"w-3.5 h-3.5\" />\n                ) : (\n                  <EyeOff className=\"w-3.5 h-3.5\" />\n                )}\n              </button>\n              {!readOnly && (\n                <button\n                  type=\"button\"\n                  onClick={() => handleRemove(index)}\n                  className=\"p-1 text-muted-foreground hover:text-red-500 transition-colors shrink-0\"\n                  aria-label={`Remove ${variable.key || \"variable\"}`}\n                >\n                  <Trash2 className=\"w-3.5 h-3.5\" />\n                </button>\n              )}\n            </div>\n          </div>\n        ))}\n\n        {variables.length === 0 && (\n          <div className=\"px-3 py-4 text-sm text-muted-foreground text-center\">\n            No environment variables\n          </div>\n        )}\n      </div>\n\n      {!readOnly && (\n        <div className=\"px-3 py-2 border-t border-border\">\n          <button\n            type=\"button\"\n            onClick={handleAdd}\n            aria-label=\"Add new environment variable\"\n            className=\"flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground transition-colors\"\n          >\n            <Plus className=\"w-4 h-4\" />\n            Add variable\n          </button>\n        </div>\n      )}\n    </div>\n  );\n}\n\nexport type { EnvEditorProps, EnvVariable };\n",
      "type": "registry:component"
    }
  ],
  "docs": "Key-value editing grid, password masking, .env file import/export, add/remove variables.",
  "categories": [
    "devtools"
  ],
  "type": "registry:ui"
}