{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "environment-variables-manager",
  "title": "Environment Variables Manager",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-angular/blocks/environment-variables-manager/environment-variables-manager.component.ts",
      "content": "// Vercel / Supabase style environment variables manager: environment-scoped tabs with counts,\n// search + type filter, mask/reveal and copy per value, an inline \"add variable\" card, an edit\n// dialog, bulk .env import with a live parse preview, and .env export. Port of the React block 1:1.\nimport { ChangeDetectionStrategy, Component, DestroyRef, Input, computed, inject, signal } from '@angular/core'\nimport {\n  AlertCircle,\n  Check,\n  Copy,\n  Download,\n  Eye,\n  EyeOff,\n  FileCode,\n  KeyRound,\n  Lock,\n  LucideAngularModule,\n  MoreHorizontal,\n  Pencil,\n  Plus,\n  Search,\n  ShieldCheck,\n  Trash2,\n  Upload,\n  X,\n} from 'lucide-angular'\nimport { cn } from '@/lib/utils'\nimport { UiBadgeComponent } from '@/ui/badge/badge.component'\nimport { UiButtonComponent } from '@/ui/button/button.component'\nimport {\n  UiCardComponent,\n  UiCardContentComponent,\n  UiCardDescriptionComponent,\n  UiCardHeaderComponent,\n  UiCardTitleComponent,\n} from '@/ui/card/card.component'\nimport { UiCheckboxComponent } from '@/ui/checkbox/checkbox.component'\nimport {\n  UiDialogComponent,\n  UiDialogContentComponent,\n  UiDialogDescriptionComponent,\n  UiDialogFooterComponent,\n  UiDialogHeaderComponent,\n  UiDialogTitleComponent,\n} from '@/ui/dialog/dialog.component'\nimport {\n  UiDropdownMenuComponent,\n  UiDropdownMenuContentComponent,\n  UiDropdownMenuItemComponent,\n  UiDropdownMenuSeparatorComponent,\n  UiDropdownMenuTriggerComponent,\n} from '@/ui/dropdown-menu/dropdown-menu.component'\nimport { UiInputComponent } from '@/ui/input/input.component'\nimport {\n  UiSelectComponent,\n  UiSelectContentComponent,\n  UiSelectItemComponent,\n  UiSelectTriggerComponent,\n  UiSelectValueComponent,\n} from '@/ui/select/select.component'\nimport {\n  UiTableBodyComponent,\n  UiTableCellComponent,\n  UiTableComponent,\n  UiTableHeadComponent,\n  UiTableHeaderComponent,\n  UiTableRowComponent,\n} from '@/ui/table/table.component'\nimport { UiTabsComponent, UiTabsListComponent, UiTabsTriggerComponent } from '@/ui/tabs/tabs.component'\nimport { UiTextareaComponent } from '@/ui/textarea/textarea.component'\n\nexport type EnvironmentScope = 'production' | 'preview' | 'development'\nexport type EnvVarTypeFilter = 'all' | 'secret' | 'plain'\n\nexport interface EnvironmentVariable {\n  id: string\n  key: string\n  value: string\n  environments: EnvironmentScope[]\n  isSecret: boolean\n  updatedAt: string\n  updatedBy: string\n}\n\nconst defaultVariables: EnvironmentVariable[] = [\n  {\n    id: 'env-1',\n    key: 'DATABASE_URL',\n    value: 'postgresql://postgres:p4ssw0rd_secure_vault@db.prod.aws.internal:5432/primary_db',\n    environments: ['production', 'preview', 'development'],\n    isSecret: true,\n    updatedAt: 'Updated 2h ago',\n    updatedBy: 'alex.chen',\n  },\n  {\n    id: 'env-2',\n    key: 'STRIPE_SECRET_KEY',\n    value: 'mock_key_51NwY2xK9mPqL8vR4tZa0bCeFgHiJkLmNoPqRsTuVwXyZ',\n    environments: ['production'],\n    isSecret: true,\n    updatedAt: 'Updated 3d ago',\n    updatedBy: 'sarah.dev',\n  },\n  {\n    id: 'env-3',\n    key: 'NEXT_PUBLIC_APP_URL',\n    value: 'https://app.uipkge.dev',\n    environments: ['production', 'preview', 'development'],\n    isSecret: false,\n    updatedAt: 'Updated 5d ago',\n    updatedBy: 'uday.craft',\n  },\n  {\n    id: 'env-4',\n    key: 'REDIS_PASSWORD',\n    value: 'redis_auth_98f4b6201e9d4a87b32c',\n    environments: ['production', 'preview'],\n    isSecret: true,\n    updatedAt: 'Updated 1w ago',\n    updatedBy: 'alex.chen',\n  },\n  {\n    id: 'env-5',\n    key: 'SENTRY_DSN',\n    value: 'https://o45089@sentry.io/45089234871923',\n    environments: ['production', 'preview', 'development'],\n    isSecret: false,\n    updatedAt: 'Updated 2w ago',\n    updatedBy: 'sarah.dev',\n  },\n  {\n    id: 'env-6',\n    key: 'AWS_SECRET_ACCESS_KEY',\n    value: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',\n    environments: ['production'],\n    isSecret: true,\n    updatedAt: 'Updated 1mo ago',\n    updatedBy: 'infra-bot',\n  },\n]\n\nfunction maskString(val: string): string {\n  if (val.length <= 12) return '••••••••••••'\n  return '••••••••••••••••••••'\n}\n\nconst ALL_ENVS: EnvironmentScope[] = ['production', 'preview', 'development']\n\ninterface ScopeOption {\n  value: EnvironmentScope\n  label: string\n  /** Label border/background when the scope is checked. */\n  active: string\n  /** Label text colour. */\n  text: string\n}\n\nconst scopeOptions: ScopeOption[] = [\n  { value: 'production', label: 'Production', active: 'border-success/40 bg-success/5', text: 'text-success' },\n  { value: 'preview', label: 'Preview', active: 'border-info/40 bg-info/5', text: 'text-info' },\n  { value: 'development', label: 'Development', active: 'border-chart-2/40 bg-chart-2/5', text: 'text-chart-2' },\n]\n\ninterface ParsedImportItem {\n  key: string\n  value: string\n  isSecret: boolean\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-environment-variables-manager, [ui-environment-variables-manager]',\n  standalone: true,\n  // React renders the root <div> itself: the host stays out of layout and `class` goes to the root.\n  host: { '[attr.class]': '\"contents\"' },\n  imports: [\n    LucideAngularModule,\n    UiBadgeComponent,\n    UiButtonComponent,\n    UiCardComponent,\n    UiCardContentComponent,\n    UiCardDescriptionComponent,\n    UiCardHeaderComponent,\n    UiCardTitleComponent,\n    UiCheckboxComponent,\n    UiDialogComponent,\n    UiDialogContentComponent,\n    UiDialogDescriptionComponent,\n    UiDialogFooterComponent,\n    UiDialogHeaderComponent,\n    UiDialogTitleComponent,\n    UiDropdownMenuComponent,\n    UiDropdownMenuContentComponent,\n    UiDropdownMenuItemComponent,\n    UiDropdownMenuSeparatorComponent,\n    UiDropdownMenuTriggerComponent,\n    UiInputComponent,\n    UiSelectComponent,\n    UiSelectContentComponent,\n    UiSelectItemComponent,\n    UiSelectTriggerComponent,\n    UiSelectValueComponent,\n    UiTableComponent,\n    UiTableBodyComponent,\n    UiTableCellComponent,\n    UiTableHeadComponent,\n    UiTableHeaderComponent,\n    UiTableRowComponent,\n    UiTabsComponent,\n    UiTabsListComponent,\n    UiTabsTriggerComponent,\n    UiTextareaComponent,\n  ],\n  template: `\n    <div data-slot=\"environment-variables-manager\" [class]=\"rootClass\">\n      <!-- Header Section -->\n      <div class=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n        <div class=\"space-y-1\">\n          <div class=\"flex items-center gap-2.5\">\n            <div class=\"bg-primary/10 text-primary grid size-8 place-items-center rounded-lg border\">\n              <lucide-icon [img]=\"KeyRound\" class=\"size-4\" />\n            </div>\n            <h2 class=\"text-xl font-semibold tracking-tight sm:text-2xl\">Environment Variables</h2>\n          </div>\n          <p class=\"text-muted-foreground text-sm\">\n            Manage encrypted secrets, API keys, and configuration for your deployments.\n          </p>\n        </div>\n\n        <div class=\"flex flex-wrap items-center gap-2\">\n          <button ui-button variant=\"outline\" size=\"sm\" class=\"gap-1.5 shadow-xs\" (click)=\"isImportOpen.set(true)\">\n            <lucide-icon [img]=\"Upload\" class=\"size-3.5\" />\n            <span>Import .env</span>\n          </button>\n          <button\n            ui-button\n            aria-label=\"Download attachment\"\n            variant=\"outline\"\n            size=\"sm\"\n            class=\"gap-1.5 shadow-xs\"\n            (click)=\"exportEnvFile()\"\n          >\n            <lucide-icon [img]=\"Download\" class=\"size-3.5\" />\n            <span>Export</span>\n          </button>\n          <button\n            ui-button\n            size=\"sm\"\n            class=\"gap-1.5 shadow-xs\"\n            [variant]=\"isAddOpen() ? 'secondary' : 'default'\"\n            (click)=\"isAddOpen.set(!isAddOpen())\"\n          >\n            @if (!isAddOpen()) {\n              <lucide-icon [img]=\"Plus\" class=\"size-4\" />\n            } @else {\n              <lucide-icon [img]=\"X\" class=\"size-4\" />\n            }\n            <span>{{ isAddOpen() ? 'Close Form' : 'Add Variable' }}</span>\n          </button>\n        </div>\n      </div>\n\n      <!-- Add Variable Expandable Card -->\n      @if (isAddOpen()) {\n        <div ui-card class=\"border-primary/30 bg-card/60 relative shadow-sm\">\n          <div ui-card-header class=\"pb-3\">\n            <div class=\"flex items-center justify-between\">\n              <div>\n                <h3 ui-card-title class=\"text-base font-semibold\">New Environment Variable</h3>\n                <p ui-card-description class=\"text-xs\">\n                  Add a new secret key or configuration parameter to your environment scopes.\n                </p>\n              </div>\n              <button\n                ui-button\n                variant=\"ghost\"\n                size=\"icon-sm\"\n                aria-label=\"Cancel adding variable\"\n                (click)=\"resetAddForm()\"\n              >\n                <lucide-icon [img]=\"X\" class=\"size-4\" />\n              </button>\n            </div>\n          </div>\n          <div ui-card-content class=\"space-y-4\">\n            <div class=\"grid grid-cols-1 gap-4 md:grid-cols-2\">\n              <div class=\"space-y-1.5\">\n                <label class=\"text-foreground text-xs font-medium\">Variable Key</label>\n                <ui-input\n                  [value]=\"newKey()\"\n                  (valueChange)=\"newKey.set($event)\"\n                  placeholder=\"e.g. STRIPE_SECRET_KEY\"\n                  class=\"font-mono text-xs uppercase\"\n                  (keydown.enter)=\"handleAddVariable()\"\n                />\n                <p class=\"text-muted-foreground text-xs\">Uppercase characters and underscores recommended.</p>\n              </div>\n              <div class=\"space-y-1.5\">\n                <label class=\"text-foreground text-xs font-medium\">Variable Value</label>\n                <ui-input\n                  [value]=\"newValue()\"\n                  (valueChange)=\"newValue.set($event)\"\n                  [type]=\"newIsSecret() ? 'password' : 'text'\"\n                  placeholder=\"Enter secret token or value...\"\n                  class=\"font-mono text-xs\"\n                  (keydown.enter)=\"handleAddVariable()\"\n                />\n                <p class=\"text-muted-foreground text-xs\">Values are encrypted at rest with AES-256-GCM.</p>\n              </div>\n            </div>\n\n            <div class=\"flex flex-col gap-4 pt-1 sm:flex-row sm:items-center sm:justify-between\">\n              <div class=\"space-y-2\">\n                <span class=\"text-foreground text-xs font-medium\">Environment Scopes</span>\n                <div class=\"flex flex-wrap items-center gap-3\">\n                  @for (scope of scopeOptions; track scope.value) {\n                    <label [class]=\"scopeLabelClass(newEnvs(), scope, 'gap-2 transition-colors')\">\n                      <ui-checkbox\n                        [checked]=\"newEnvs().includes(scope.value)\"\n                        (checkedChange)=\"toggleNewEnv(scope.value)\"\n                      />\n                      <span [class]=\"scope.text + ' font-medium'\">{{ scope.label }}</span>\n                    </label>\n                  }\n                </div>\n              </div>\n\n              <div class=\"flex items-center gap-2\">\n                <ui-checkbox\n                  id=\"angular-new-is-secret\"\n                  [checked]=\"newIsSecret()\"\n                  (checkedChange)=\"newIsSecret.set(!!$event)\"\n                />\n                <label for=\"angular-new-is-secret\" class=\"cursor-pointer text-xs font-medium select-none\">\n                  Encrypt as Secret\n                </label>\n              </div>\n            </div>\n\n            <div class=\"flex items-center justify-end gap-2 border-t pt-3\">\n              <button ui-button aria-label=\"Cancel adding variable\" variant=\"ghost\" size=\"sm\" (click)=\"resetAddForm()\">\n                Cancel\n              </button>\n              <button\n                ui-button\n                size=\"sm\"\n                [disabled]=\"!newKey().trim() || !newValue().trim()\"\n                (click)=\"handleAddVariable()\"\n              >\n                Save Variable\n              </button>\n            </div>\n          </div>\n        </div>\n      }\n\n      <!-- Environment Tabs & Search / Filter Controls -->\n      <div class=\"space-y-3\">\n        <ui-tabs [value]=\"activeTab()\" (valueChange)=\"setActiveTab($event)\" class=\"w-full\">\n          <ui-tabs-list class=\"w-full justify-start overflow-x-auto sm:w-auto\">\n            <button ui-tabs-trigger value=\"all\" class=\"text-xs\">\n              All Environments\n              <span ui-badge variant=\"secondary\" class=\"ml-1.5 px-1.5 py-0 text-xs font-semibold\">\n                {{ counts().all }}\n              </span>\n            </button>\n            <button ui-tabs-trigger value=\"production\" class=\"text-xs\">\n              Production\n              <span ui-badge variant=\"secondary\" class=\"ml-1.5 px-1.5 py-0 text-xs font-semibold\">\n                {{ counts().production }}\n              </span>\n            </button>\n            <button ui-tabs-trigger value=\"preview\" class=\"text-xs\">\n              Preview\n              <span ui-badge variant=\"secondary\" class=\"ml-1.5 px-1.5 py-0 text-xs font-semibold\">\n                {{ counts().preview }}\n              </span>\n            </button>\n            <button ui-tabs-trigger value=\"development\" class=\"text-xs\">\n              Development\n              <span ui-badge variant=\"secondary\" class=\"ml-1.5 px-1.5 py-0 text-xs font-semibold\">\n                {{ counts().development }}\n              </span>\n            </button>\n          </ui-tabs-list>\n        </ui-tabs>\n\n        <div class=\"flex flex-col gap-2.5 sm:flex-row sm:items-center sm:justify-between\">\n          <div class=\"flex flex-1 flex-wrap items-center gap-2\">\n            <div class=\"relative max-w-sm min-w-[200px] flex-1\">\n              <lucide-icon\n                [img]=\"Search\"\n                class=\"text-muted-foreground pointer-events-none absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2\"\n              />\n              <ui-input\n                [value]=\"searchQuery()\"\n                (valueChange)=\"searchQuery.set($event)\"\n                placeholder=\"Search variables by key...\"\n                class=\"h-8.5 pl-8 text-xs shadow-xs\"\n              />\n            </div>\n\n            <ui-select [value]=\"typeFilter()\" (valueChange)=\"setTypeFilter($event)\">\n              <button ui-select-trigger class=\"h-8.5 w-[140px] text-xs shadow-xs\">\n                <ui-select-value placeholder=\"All types\" />\n              </button>\n              <ui-select-content>\n                <ui-select-item value=\"all\">All Types</ui-select-item>\n                <ui-select-item value=\"secret\">Encrypted Secrets</ui-select-item>\n                <ui-select-item value=\"plain\">Plaintext</ui-select-item>\n              </ui-select-content>\n            </ui-select>\n          </div>\n\n          <div class=\"flex items-center gap-2\">\n            <button ui-button variant=\"outline\" size=\"sm\" [class]=\"revealAllClass\" (click)=\"toggleRevealAll()\">\n              @if (revealAll()) {\n                <lucide-icon [img]=\"EyeOff\" class=\"size-3.5\" />\n              } @else {\n                <lucide-icon [img]=\"Eye\" class=\"size-3.5\" />\n              }\n              <span>{{ revealAll() ? 'Hide All' : 'Reveal All Values' }}</span>\n            </button>\n          </div>\n        </div>\n      </div>\n\n      <!-- Variables Table -->\n      <div class=\"bg-card overflow-hidden rounded-lg border shadow-xs\">\n        <div class=\"overflow-x-auto\">\n          <table ui-table>\n            <thead ui-table-header>\n              <tr ui-table-row class=\"bg-muted/40 hover:bg-muted/40\">\n                <th ui-table-head class=\"text-muted-foreground text-xs font-medium\">Variable Key</th>\n                <th ui-table-head class=\"text-muted-foreground text-xs font-medium\">Value</th>\n                <th ui-table-head class=\"text-muted-foreground text-xs font-medium\">Environments</th>\n                <th ui-table-head class=\"text-muted-foreground text-xs font-medium\">Type</th>\n                <th ui-table-head class=\"text-muted-foreground hidden text-xs font-medium md:table-cell\">Updated</th>\n                <th ui-table-head class=\"w-[70px] text-right text-xs font-medium\">\n                  <span class=\"sr-only\">Actions</span>\n                </th>\n              </tr>\n            </thead>\n            <tbody ui-table-body>\n              @for (item of filteredVariables(); track item.id) {\n                <tr ui-table-row class=\"group hover:bg-muted/30 transition-colors\">\n                  <!-- Key -->\n                  <td ui-table-cell class=\"py-3.5\">\n                    <div class=\"flex items-center gap-2\">\n                      <span\n                        class=\"text-foreground font-mono text-xs font-semibold tracking-tight select-all sm:text-sm\"\n                      >\n                        {{ item.key }}\n                      </span>\n                      <button\n                        type=\"button\"\n                        aria-label=\"Copy variable key\"\n                        class=\"text-muted-foreground hover:text-foreground focus-visible:ring-ring rounded-sm opacity-0 transition-opacity outline-none group-hover:opacity-100 focus:opacity-100 focus-visible:ring-2\"\n                        (click)=\"copyValue(item.key, 'key-' + item.id)\"\n                      >\n                        @if (copiedId() === 'key-' + item.id) {\n                          <lucide-icon [img]=\"Check\" class=\"text-success size-3.5\" />\n                        } @else {\n                          <lucide-icon [img]=\"Copy\" class=\"size-3.5\" />\n                        }\n                      </button>\n                    </div>\n                  </td>\n\n                  <!-- Value Mask / Plain -->\n                  <td ui-table-cell class=\"py-3.5\">\n                    <div class=\"flex max-w-[280px] items-center gap-1.5 sm:max-w-xs md:max-w-sm\">\n                      <code\n                        class=\"bg-muted/60 text-foreground max-w-[200px] min-w-0 truncate rounded border px-2 py-1 font-mono text-xs sm:max-w-[240px]\"\n                        [attr.title]=\"isRevealed(item.id) ? item.value : 'Masked secret value'\"\n                        >{{ isRevealed(item.id) ? item.value : mask(item.value) }}</code\n                      >\n                      <button\n                        ui-button\n                        variant=\"ghost\"\n                        size=\"icon-xs\"\n                        class=\"text-muted-foreground hover:text-foreground size-7 shrink-0\"\n                        [attr.aria-label]=\"isRevealed(item.id) ? 'Hide value' : 'Reveal value'\"\n                        (click)=\"toggleReveal(item.id)\"\n                      >\n                        @if (isRevealed(item.id)) {\n                          <lucide-icon [img]=\"EyeOff\" class=\"size-3.5\" />\n                        } @else {\n                          <lucide-icon [img]=\"Eye\" class=\"size-3.5\" />\n                        }\n                      </button>\n                      <button\n                        ui-button\n                        variant=\"ghost\"\n                        size=\"icon-xs\"\n                        class=\"text-muted-foreground hover:text-foreground size-7 shrink-0\"\n                        [attr.aria-label]=\"copiedId() === item.id ? 'Copied' : 'Copy value'\"\n                        (click)=\"copyValue(item.value, item.id)\"\n                      >\n                        @if (copiedId() === item.id) {\n                          <lucide-icon [img]=\"Check\" class=\"text-success size-3.5\" />\n                        } @else {\n                          <lucide-icon [img]=\"Copy\" class=\"size-3.5\" />\n                        }\n                      </button>\n                    </div>\n                  </td>\n\n                  <!-- Environments -->\n                  <td ui-table-cell class=\"py-3.5\">\n                    <div class=\"flex flex-wrap items-center gap-1\">\n                      @if (item.environments.includes('production')) {\n                        <span\n                          ui-badge\n                          variant=\"outline\"\n                          class=\"border-success/30 bg-success/10 text-success px-2 py-0 text-xs font-medium\"\n                        >\n                          Production\n                        </span>\n                      }\n                      @if (item.environments.includes('preview')) {\n                        <span\n                          ui-badge\n                          variant=\"outline\"\n                          class=\"border-info/30 bg-info/10 text-info px-2 py-0 text-xs font-medium\"\n                        >\n                          Preview\n                        </span>\n                      }\n                      @if (item.environments.includes('development')) {\n                        <span\n                          ui-badge\n                          variant=\"outline\"\n                          class=\"border-chart-2/30 bg-chart-2/10 text-chart-2 px-2 py-0 text-xs font-medium\"\n                        >\n                          Development\n                        </span>\n                      }\n                    </div>\n                  </td>\n\n                  <!-- Type -->\n                  <td ui-table-cell class=\"py-3.5\">\n                    @if (item.isSecret) {\n                      <span\n                        ui-badge\n                        variant=\"secondary\"\n                        class=\"border-warning/30 bg-warning/10 text-warning gap-1 px-2 py-0 text-xs font-medium\"\n                      >\n                        <lucide-icon [img]=\"Lock\" class=\"size-3\" />\n                        Encrypted Secret\n                      </span>\n                    } @else {\n                      <span\n                        ui-badge\n                        variant=\"outline\"\n                        class=\"text-muted-foreground gap-1 px-2 py-0 text-xs font-medium\"\n                      >\n                        <lucide-icon [img]=\"FileCode\" class=\"size-3\" />\n                        Plaintext\n                      </span>\n                    }\n                  </td>\n\n                  <!-- Updated -->\n                  <td ui-table-cell class=\"text-muted-foreground hidden py-3.5 text-xs whitespace-nowrap md:table-cell\">\n                    <span>{{ item.updatedAt }}</span>\n                    <span class=\"block text-xs opacity-75\">by {{ item.updatedBy }}</span>\n                  </td>\n\n                  <!-- Actions Dropdown -->\n                  <td ui-table-cell class=\"py-3.5 text-right\">\n                    <ui-dropdown-menu>\n                      <button\n                        ui-button\n                        ui-dropdown-menu-trigger\n                        variant=\"ghost\"\n                        size=\"icon-sm\"\n                        class=\"text-muted-foreground hover:text-foreground size-8\"\n                      >\n                        <lucide-icon [img]=\"MoreHorizontal\" class=\"size-4\" />\n                        <span class=\"sr-only\">Actions for {{ item.key }}</span>\n                      </button>\n                      <ui-dropdown-menu-content align=\"end\" class=\"w-44 text-xs\">\n                        <ui-dropdown-menu-item (select)=\"openEdit(item)\">\n                          <lucide-icon [img]=\"Pencil\" class=\"mr-2 size-3.5\" />\n                          <span>Edit variable</span>\n                        </ui-dropdown-menu-item>\n                        <ui-dropdown-menu-item (select)=\"handleDuplicate(item)\">\n                          <lucide-icon [img]=\"Copy\" class=\"mr-2 size-3.5\" />\n                          <span>Duplicate</span>\n                        </ui-dropdown-menu-item>\n                        <ui-dropdown-menu-item (select)=\"copyValue(item.value, item.id)\">\n                          @if (copiedId() === item.id) {\n                            <lucide-icon [img]=\"Check\" class=\"text-success mr-2 size-3.5\" />\n                          } @else {\n                            <lucide-icon [img]=\"Copy\" class=\"mr-2 size-3.5\" />\n                          }\n                          <span>Copy value</span>\n                        </ui-dropdown-menu-item>\n                        <ui-dropdown-menu-separator />\n                        <ui-dropdown-menu-item\n                          class=\"text-destructive focus:text-destructive\"\n                          (select)=\"handleDelete(item.id)\"\n                        >\n                          <lucide-icon [img]=\"Trash2\" class=\"mr-2 size-3.5\" />\n                          <span>Delete</span>\n                        </ui-dropdown-menu-item>\n                      </ui-dropdown-menu-content>\n                    </ui-dropdown-menu>\n                  </td>\n                </tr>\n              }\n\n              <!-- Empty State -->\n              @if (filteredVariables().length === 0) {\n                <tr ui-table-row>\n                  <td ui-table-cell [attr.colspan]=\"6\" class=\"h-44 text-center\">\n                    <div class=\"flex flex-col items-center justify-center space-y-2 py-6\">\n                      <div class=\"bg-muted text-muted-foreground grid size-10 place-items-center rounded-full border\">\n                        <lucide-icon [img]=\"AlertCircle\" class=\"size-5\" />\n                      </div>\n                      <p class=\"text-foreground text-sm font-medium\">No environment variables found</p>\n                      <p class=\"text-muted-foreground max-w-xs text-xs\">\n                        {{\n                          isFiltered()\n                            ? 'Try adjusting your filters or search keywords.'\n                            : 'Add your first environment secret or configuration key.'\n                        }}\n                      </p>\n                      @if (!isFiltered()) {\n                        <button ui-button size=\"sm\" class=\"mt-2 text-xs\" (click)=\"isAddOpen.set(true)\">\n                          <lucide-icon [img]=\"Plus\" class=\"mr-1.5 size-3.5\" />\n                          Add Variable\n                        </button>\n                      }\n                    </div>\n                  </td>\n                </tr>\n              }\n            </tbody>\n          </table>\n        </div>\n      </div>\n\n      <!-- Edit Variable Dialog -->\n      <ui-dialog [open]=\"!!editingVariable()\" (openChange)=\"onEditOpenChange($event)\">\n        @if (editingVariable(); as ev) {\n          <ui-dialog-content class=\"sm:max-w-md\">\n            <div ui-dialog-header>\n              <h2 ui-dialog-title>Edit Environment Variable</h2>\n              <p ui-dialog-description>Update variable value and configured environment scopes.</p>\n            </div>\n            <div class=\"space-y-4 py-2\">\n              <div class=\"space-y-1.5\">\n                <label class=\"text-xs font-medium\">Variable Key</label>\n                <ui-input\n                  [value]=\"ev.key\"\n                  (valueChange)=\"patchEditing({ key: $event })\"\n                  class=\"font-mono text-xs uppercase\"\n                />\n              </div>\n\n              <div class=\"space-y-1.5\">\n                <label class=\"text-xs font-medium\">Variable Value</label>\n                <ui-input\n                  [value]=\"ev.value\"\n                  (valueChange)=\"patchEditing({ value: $event })\"\n                  [type]=\"ev.isSecret ? 'password' : 'text'\"\n                  class=\"font-mono text-xs\"\n                />\n              </div>\n\n              <div class=\"space-y-2\">\n                <span class=\"text-xs font-medium\">Environments</span>\n                <div class=\"flex flex-wrap items-center gap-2\">\n                  @for (scope of scopeOptions; track scope.value) {\n                    <label [class]=\"scopeLabelClass(ev.environments, scope, 'gap-1.5')\">\n                      <ui-checkbox\n                        [checked]=\"ev.environments.includes(scope.value)\"\n                        (checkedChange)=\"toggleEditEnv(scope.value)\"\n                      />\n                      <span [class]=\"scope.text + ' font-medium'\">{{ scope.label }}</span>\n                    </label>\n                  }\n                </div>\n              </div>\n\n              <div class=\"flex items-center gap-2 pt-1\">\n                <ui-checkbox\n                  id=\"angular-edit-is-secret\"\n                  [checked]=\"ev.isSecret\"\n                  (checkedChange)=\"patchEditing({ isSecret: !!$event })\"\n                />\n                <label for=\"angular-edit-is-secret\" class=\"cursor-pointer text-xs font-medium select-none\">\n                  Encrypt as Secret\n                </label>\n              </div>\n            </div>\n            <div ui-dialog-footer>\n              <button ui-button variant=\"ghost\" size=\"sm\" (click)=\"editingVariable.set(null)\">Cancel</button>\n              <button ui-button size=\"sm\" (click)=\"saveEdit()\">Save Changes</button>\n            </div>\n          </ui-dialog-content>\n        }\n      </ui-dialog>\n\n      <!-- Import .env Dialog -->\n      <ui-dialog [open]=\"isImportOpen()\" (openChange)=\"isImportOpen.set($event)\">\n        <ui-dialog-content class=\"sm:max-w-lg\">\n          <div ui-dialog-header>\n            <div class=\"flex items-center gap-2\">\n              <div class=\"bg-primary/10 text-primary grid size-7 place-items-center rounded-md border\">\n                <lucide-icon [img]=\"Upload\" class=\"size-3.5\" />\n              </div>\n              <h2 ui-dialog-title>Import .env File</h2>\n            </div>\n            <p ui-dialog-description>\n              Paste your raw .env, .env.local, or .env.production file contents to bulk add variables.\n            </p>\n          </div>\n\n          <div class=\"space-y-4 py-2\">\n            <div class=\"space-y-1.5\">\n              <label class=\"text-xs font-medium\">.env File Content</label>\n              <ui-textarea\n                [value]=\"importText()\"\n                (valueChange)=\"importText.set($event)\"\n                [rows]=\"6\"\n                placeholder=\"DATABASE_URL=postgresql://user:pass@host:5432/db&#10;STRIPE_SECRET_KEY=mock_key_...&#10;NEXT_PUBLIC_APP_URL=https://app.example.com\"\n                class=\"font-mono text-xs\"\n              />\n            </div>\n\n            <div class=\"space-y-2\">\n              <span class=\"text-xs font-medium\">Assign to Environments</span>\n              <div class=\"flex flex-wrap items-center gap-2\">\n                @for (scope of scopeOptions; track scope.value) {\n                  <label [class]=\"scopeLabelClass(importEnvs(), scope, 'gap-1.5')\">\n                    <ui-checkbox\n                      [checked]=\"importEnvs().includes(scope.value)\"\n                      (checkedChange)=\"toggleImportEnv(scope.value)\"\n                    />\n                    <span [class]=\"scope.text + ' font-medium'\">{{ scope.label }}</span>\n                  </label>\n                }\n              </div>\n            </div>\n\n            <div class=\"flex items-center gap-2 pt-1\">\n              <ui-checkbox\n                id=\"angular-import-encrypt-all\"\n                [checked]=\"importEncryptAll()\"\n                (checkedChange)=\"importEncryptAll.set(!!$event)\"\n              />\n              <label for=\"angular-import-encrypt-all\" class=\"cursor-pointer text-xs font-medium select-none\">\n                Encrypt all variables by default\n              </label>\n            </div>\n\n            @if (parsedImportItems().length > 0) {\n              <div class=\"bg-muted/60 flex items-center gap-2 rounded-md border px-3 py-2 text-xs\">\n                <lucide-icon [img]=\"ShieldCheck\" class=\"text-success size-4 shrink-0\" />\n                <span>\n                  Ready to import <strong>{{ parsedImportItems().length }}</strong> variable(s).\n                </span>\n              </div>\n            }\n          </div>\n\n          <div ui-dialog-footer>\n            <button ui-button variant=\"ghost\" size=\"sm\" (click)=\"isImportOpen.set(false)\">Cancel</button>\n            <button ui-button size=\"sm\" [disabled]=\"parsedImportItems().length === 0\" (click)=\"handleImport()\">\n              Import {{ parsedImportItems().length > 0 ? '(' + parsedImportItems().length + ')' : '' }}\n            </button>\n          </div>\n        </ui-dialog-content>\n      </ui-dialog>\n    </div>\n  `,\n})\nexport class UiEnvironmentVariablesManagerComponent {\n  protected readonly AlertCircle = AlertCircle\n  protected readonly Check = Check\n  protected readonly Copy = Copy\n  protected readonly Download = Download\n  protected readonly Eye = Eye\n  protected readonly EyeOff = EyeOff\n  protected readonly FileCode = FileCode\n  protected readonly KeyRound = KeyRound\n  protected readonly Lock = Lock\n  protected readonly MoreHorizontal = MoreHorizontal\n  protected readonly Pencil = Pencil\n  protected readonly Plus = Plus\n  protected readonly Search = Search\n  protected readonly ShieldCheck = ShieldCheck\n  protected readonly Trash2 = Trash2\n  protected readonly Upload = Upload\n  protected readonly X = X\n\n  protected readonly scopeOptions = scopeOptions\n\n  @Input() set initialVariables(v: EnvironmentVariable[] | undefined) {\n    this.variables.set((v ?? defaultVariables).map((item) => ({ ...item, environments: [...item.environments] })))\n  }\n  @Input() set initialAddOpen(v: boolean) {\n    this.isAddOpen.set(!!v)\n  }\n  @Input() set initialImportOpen(v: boolean) {\n    this.isImportOpen.set(!!v)\n  }\n  @Input() set defaultEnvironment(v: 'all' | EnvironmentScope) {\n    this.activeTab.set(v ?? 'all')\n  }\n  @Input('class') className?: string\n\n  readonly variables = signal<EnvironmentVariable[]>(\n    defaultVariables.map((v) => ({ ...v, environments: [...v.environments] })),\n  )\n\n  readonly activeTab = signal<'all' | EnvironmentScope>('all')\n  readonly searchQuery = signal('')\n  readonly typeFilter = signal<EnvVarTypeFilter>('all')\n  readonly revealAll = signal(false)\n  readonly revealedMap = signal<Record<string, boolean>>({})\n\n  readonly isAddOpen = signal(false)\n  readonly isImportOpen = signal(false)\n  readonly editingVariable = signal<EnvironmentVariable | null>(null)\n\n  // Add variable state\n  readonly newKey = signal('')\n  readonly newValue = signal('')\n  readonly newIsSecret = signal(true)\n  readonly newEnvs = signal<EnvironmentScope[]>([...ALL_ENVS])\n\n  // Import state\n  readonly importText = signal('')\n  readonly importEnvs = signal<EnvironmentScope[]>([...ALL_ENVS])\n  readonly importEncryptAll = signal(true)\n\n  // Copy state\n  readonly copiedId = signal<string | null>(null)\n  private copyTimer: ReturnType<typeof setTimeout> | undefined\n  // Monotonic suffix so ids stay unique even when two actions land in the same millisecond.\n  private uid = 0\n\n  private nextId(prefix: string): string {\n    return `${prefix}-${Date.now()}-${this.uid++}`\n  }\n\n  constructor() {\n    inject(DestroyRef).onDestroy(() => clearTimeout(this.copyTimer))\n  }\n\n  readonly filteredVariables = computed(() => {\n    const tab = this.activeTab()\n    const type = this.typeFilter()\n    const q = this.searchQuery().toLowerCase().trim()\n    return this.variables().filter((item) => {\n      if (tab !== 'all' && !item.environments.includes(tab)) return false\n      if (type === 'secret' && !item.isSecret) return false\n      if (type === 'plain' && item.isSecret) return false\n      if (q) {\n        const keyMatch = item.key.toLowerCase().includes(q)\n        const authorMatch = item.updatedBy.toLowerCase().includes(q)\n        if (!keyMatch && !authorMatch) return false\n      }\n      return true\n    })\n  })\n\n  readonly counts = computed(() => {\n    const vars = this.variables()\n    return {\n      all: vars.length,\n      production: vars.filter((v) => v.environments.includes('production')).length,\n      preview: vars.filter((v) => v.environments.includes('preview')).length,\n      development: vars.filter((v) => v.environments.includes('development')).length,\n    }\n  })\n\n  readonly isFiltered = computed(\n    () => !!this.searchQuery() || this.typeFilter() !== 'all' || this.activeTab() !== 'all',\n  )\n\n  readonly parsedImportItems = computed<ParsedImportItem[]>(() => {\n    const text = this.importText()\n    const encryptAll = this.importEncryptAll()\n    if (!text.trim()) return []\n    const results: ParsedImportItem[] = []\n    for (const rawLine of text.split('\\n')) {\n      const line = rawLine.trim()\n      if (!line || line.startsWith('#')) continue\n      const eqIdx = line.indexOf('=')\n      if (eqIdx > 0) {\n        const k = line.slice(0, eqIdx).trim().toUpperCase()\n        let v = line.slice(eqIdx + 1).trim()\n        if ((v.startsWith('\"') && v.endsWith('\"')) || (v.startsWith(\"'\") && v.endsWith(\"'\"))) {\n          v = v.slice(1, -1)\n        }\n        if (k) {\n          const isSecret =\n            encryptAll ||\n            k.includes('SECRET') ||\n            k.includes('KEY') ||\n            k.includes('PASSWORD') ||\n            k.includes('TOKEN') ||\n            k.includes('PRIVATE') ||\n            k.includes('AUTH')\n          results.push({ key: k, value: v, isSecret })\n        }\n      }\n    }\n    return results\n  })\n\n  get rootClass(): string {\n    return cn('w-full space-y-6', this.className)\n  }\n\n  get revealAllClass(): string {\n    return cn('h-8.5 gap-1.5 text-xs shadow-xs', this.revealAll() && 'border-primary text-primary')\n  }\n\n  scopeLabelClass(selected: EnvironmentScope[], scope: ScopeOption, extra: string): string {\n    return cn(\n      'hover:bg-accent/50 flex cursor-pointer items-center rounded-md border px-2.5 py-1.5 text-xs select-none',\n      extra,\n      selected.includes(scope.value) && scope.active,\n    )\n  }\n\n  mask(value: string): string {\n    return maskString(value)\n  }\n\n  setActiveTab(value: string): void {\n    this.activeTab.set(value as 'all' | EnvironmentScope)\n  }\n\n  setTypeFilter(value: string): void {\n    this.typeFilter.set(value as EnvVarTypeFilter)\n  }\n\n  isRevealed(id: string): boolean {\n    if (this.revealAll()) return true\n    return Boolean(this.revealedMap()[id])\n  }\n\n  toggleReveal(id: string): void {\n    this.revealedMap.update((prev) => ({ ...prev, [id]: !prev[id] }))\n  }\n\n  toggleRevealAll(): void {\n    const next = !this.revealAll()\n    this.revealAll.set(next)\n    if (!next) this.revealedMap.set({})\n  }\n\n  async copyValue(text: string, id: string): Promise<void> {\n    try {\n      await navigator.clipboard.writeText(text)\n      this.copiedId.set(id)\n      clearTimeout(this.copyTimer)\n      this.copyTimer = setTimeout(() => this.copiedId.set(null), 1600)\n    } catch {\n      // Clipboard fallback\n    }\n  }\n\n  private static toggleScope(list: EnvironmentScope[], env: EnvironmentScope): EnvironmentScope[] {\n    if (list.includes(env)) return list.length > 1 ? list.filter((e) => e !== env) : list\n    return [...list, env]\n  }\n\n  toggleNewEnv(env: EnvironmentScope): void {\n    this.newEnvs.update((prev) => UiEnvironmentVariablesManagerComponent.toggleScope(prev, env))\n  }\n\n  toggleImportEnv(env: EnvironmentScope): void {\n    this.importEnvs.update((prev) => UiEnvironmentVariablesManagerComponent.toggleScope(prev, env))\n  }\n\n  toggleEditEnv(env: EnvironmentScope): void {\n    const ev = this.editingVariable()\n    if (!ev) return\n    this.editingVariable.set({\n      ...ev,\n      environments: UiEnvironmentVariablesManagerComponent.toggleScope(ev.environments, env),\n    })\n  }\n\n  resetAddForm(): void {\n    this.newKey.set('')\n    this.newValue.set('')\n    this.newIsSecret.set(true)\n    this.newEnvs.set([...ALL_ENVS])\n    this.isAddOpen.set(false)\n  }\n\n  handleAddVariable(): void {\n    const key = this.newKey().trim().toUpperCase()\n    const val = this.newValue().trim()\n    if (!key || !val) return\n    const newVar: EnvironmentVariable = {\n      id: this.nextId('env'),\n      key,\n      value: val,\n      environments: [...this.newEnvs()],\n      isSecret: this.newIsSecret(),\n      updatedAt: 'Just now',\n      updatedBy: 'you',\n    }\n    this.variables.update((prev) => [newVar, ...prev])\n    this.resetAddForm()\n  }\n\n  handleDuplicate(item: EnvironmentVariable): void {\n    const duplicateVar: EnvironmentVariable = {\n      ...item,\n      environments: [...item.environments],\n      id: this.nextId('env'),\n      key: `${item.key}_COPY`,\n      updatedAt: 'Just now',\n      updatedBy: 'you',\n    }\n    this.variables.update((prev) => {\n      const index = prev.findIndex((v) => v.id === item.id)\n      if (index >= 0) {\n        const next = [...prev]\n        next.splice(index + 1, 0, duplicateVar)\n        return next\n      }\n      return [duplicateVar, ...prev]\n    })\n  }\n\n  handleDelete(id: string): void {\n    this.variables.update((prev) => prev.filter((v) => v.id !== id))\n  }\n\n  openEdit(item: EnvironmentVariable): void {\n    this.editingVariable.set({ ...item, environments: [...item.environments] })\n  }\n\n  onEditOpenChange(open: boolean): void {\n    if (!open) this.editingVariable.set(null)\n  }\n\n  patchEditing(patch: Partial<EnvironmentVariable>): void {\n    const ev = this.editingVariable()\n    if (ev) this.editingVariable.set({ ...ev, ...patch })\n  }\n\n  saveEdit(): void {\n    const ev = this.editingVariable()\n    if (!ev) return\n    const key = ev.key.trim().toUpperCase()\n    const val = ev.value.trim()\n    if (!key || !val) return\n    this.variables.update((prev) =>\n      prev.map((v) => (v.id === ev.id ? { ...ev, key, value: val, updatedAt: 'Just now', updatedBy: 'you' } : v)),\n    )\n    this.editingVariable.set(null)\n  }\n\n  handleImport(): void {\n    const items = this.parsedImportItems()\n    if (items.length === 0) return\n    const envs = this.importEnvs()\n    const stamp = Date.now()\n    const newVars: EnvironmentVariable[] = items.map((item, index) => ({\n      id: `env-imported-${stamp}-${index}`,\n      key: item.key,\n      value: item.value,\n      environments: [...envs],\n      isSecret: item.isSecret,\n      updatedAt: 'Imported just now',\n      updatedBy: 'you',\n    }))\n    this.variables.update((prev) => [...newVars, ...prev])\n    this.importText.set('')\n    this.isImportOpen.set(false)\n  }\n\n  exportEnvFile(): void {\n    const lines = this.variables().map((v) => `# ${v.environments.join(', ')}\\n${v.key}=${v.value}`)\n    const blob = new Blob([lines.join('\\n\\n')], { type: 'text/plain;charset=utf-8' })\n    const url = URL.createObjectURL(blob)\n    const a = document.createElement('a')\n    a.href = url\n    a.download = '.env.production'\n    a.click()\n    URL.revokeObjectURL(url)\n  }\n}\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/environment-variables-manager/environment-variables-manager.component.ts"
    },
    {
      "path": "packages/registry-angular/blocks/environment-variables-manager/index.ts",
      "content": "export {\n  UiEnvironmentVariablesManagerComponent,\n  type EnvironmentScope,\n  type EnvVarTypeFilter,\n  type EnvironmentVariable,\n} from './environment-variables-manager.component'\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/environment-variables-manager/index.ts"
    }
  ],
  "dependencies": [
    "lucide-angular"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/badge.json",
    "https://uipkge.dev/r/angular/button.json",
    "https://uipkge.dev/r/angular/card.json",
    "https://uipkge.dev/r/angular/checkbox.json",
    "https://uipkge.dev/r/angular/dialog.json",
    "https://uipkge.dev/r/angular/dropdown-menu.json",
    "https://uipkge.dev/r/angular/input.json",
    "https://uipkge.dev/r/angular/select.json",
    "https://uipkge.dev/r/angular/table.json",
    "https://uipkge.dev/r/angular/tabs.json",
    "https://uipkge.dev/r/angular/textarea.json"
  ],
  "description": "Vercel and Supabase style environment variables and secrets manager with environment scoping, mask/reveal toggles, bulk import, and inline editing.",
  "categories": [
    "devops",
    "dashboard",
    "data",
    "developer"
  ]
}