{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "dns-record-manager",
  "title": "Dns Record Manager",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-angular/blocks/dns-record-manager/dns-record-manager.component.ts",
      "content": "import { ChangeDetectionStrategy, Component, Input, OnDestroy, OnInit, computed, signal } from '@angular/core'\nimport {\n  Check,\n  Cloud,\n  CloudOff,\n  Copy,\n  Download,\n  Edit2,\n  FileCode,\n  Globe,\n  LucideAngularModule,\n  MoreHorizontal,\n  Plus,\n  Search,\n  ShieldAlert,\n  ShieldCheck,\n  Trash2,\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 {\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 { UiSeparatorComponent } from '@/ui/separator/separator.component'\nimport { UiSwitchComponent } from '@/ui/switch/switch.component'\nimport {\n  UiTableBodyComponent,\n  UiTableCellComponent,\n  UiTableComponent,\n  UiTableHeadComponent,\n  UiTableHeaderComponent,\n  UiTableRowComponent,\n} from '@/ui/table/table.component'\n\nexport type DnsRecordType = 'A' | 'AAAA' | 'CNAME' | 'MX' | 'TXT' | 'CAA'\n\nexport interface DnsRecord {\n  id: string\n  type: DnsRecordType\n  name: string\n  content: string\n  ttl: string\n  proxied: boolean\n  priority?: number\n  comment?: string\n}\n\nexport interface DnsRecordManagerProps {\n  domain?: string\n  nameservers?: string[]\n  dnssecEnabled?: boolean\n  initialRecords?: DnsRecord[]\n  initialAddOpen?: boolean\n  className?: string\n}\n\nconst defaultRecords: DnsRecord[] = [\n  {\n    id: 'rec-1',\n    type: 'A',\n    name: '@',\n    content: '76.76.21.21',\n    ttl: 'Auto',\n    proxied: true,\n    comment: 'Apex production load balancer',\n  },\n  {\n    id: 'rec-2',\n    type: 'CNAME',\n    name: 'www',\n    content: 'cname.vercel-dns.com',\n    ttl: 'Auto',\n    proxied: true,\n    comment: 'Primary web redirect',\n  },\n  {\n    id: 'rec-3',\n    type: 'CNAME',\n    name: 'api',\n    content: 'api-gateway.uipkge.net',\n    ttl: '300s',\n    proxied: false,\n    comment: 'Direct REST gateway route',\n  },\n  {\n    id: 'rec-4',\n    type: 'AAAA',\n    name: '@',\n    content: '2606:4700:3038::6815:1515',\n    ttl: 'Auto',\n    proxied: true,\n    comment: 'IPv6 edge ingress',\n  },\n  {\n    id: 'rec-5',\n    type: 'MX',\n    name: 'mail',\n    content: 'aspmx.l.google.com',\n    ttl: '3600s',\n    proxied: false,\n    priority: 10,\n    comment: 'Google Workspace MX',\n  },\n  {\n    id: 'rec-6',\n    type: 'TXT',\n    name: '_dmarc',\n    content: 'v=spf1 include:_spf.google.com ~all',\n    ttl: '3600s',\n    proxied: false,\n    comment: 'Sender Policy Framework verification',\n  },\n  {\n    id: 'rec-7',\n    type: 'CAA',\n    name: '@',\n    content: '0 issue \"letsencrypt.org\"',\n    ttl: 'Auto',\n    proxied: false,\n    comment: 'Certificate authority restriction',\n  },\n]\n\nconst typeBadgeStyles: Record<DnsRecordType, { bg: string; text: string; border: string }> = {\n  A: { bg: 'bg-info/10', text: 'text-info', border: 'border-info/30' },\n  AAAA: { bg: 'bg-chart-1/10', text: 'text-chart-1', border: 'border-chart-1/30' },\n  CNAME: { bg: 'bg-chart-2/10', text: 'text-chart-2', border: 'border-chart-2/30' },\n  MX: { bg: 'bg-warning/10', text: 'text-warning', border: 'border-warning/30' },\n  TXT: { bg: 'bg-success/10', text: 'text-success', border: 'border-success/30' },\n  CAA: { bg: 'bg-info/10', text: 'text-info', border: 'border-info/30' },\n}\n\nconst placeholderMap: Record<DnsRecordType, string> = {\n  A: 'e.g. 76.76.21.21',\n  AAAA: 'e.g. 2606:4700:3038::6815:1515',\n  CNAME: 'e.g. cname.vercel-dns.com',\n  MX: 'e.g. mail.google.com',\n  TXT: 'e.g. v=spf1 include:_spf.google.com ~all',\n  CAA: 'e.g. 0 issue \"letsencrypt.org\"',\n}\n\nconst contentLabelMap: Record<DnsRecordType, string> = {\n  A: 'IPv4 Address',\n  AAAA: 'IPv6 Address',\n  CNAME: 'Target Domain',\n  MX: 'Mail Server Hostname',\n  TXT: 'TXT Content / Value',\n  CAA: 'Target / Tag Value',\n}\n\nconst PROXYABLE: DnsRecordType[] = ['A', 'AAAA', 'CNAME']\n\n/**\n * Angular port of the React DnsRecordManager block: a domain header card (DNSSEC badge,\n * zone-file / add-record actions, copyable nameservers), an optional BIND zone-file viewer,\n * an add/edit record form, and a filterable records table with proxy toggles and a row\n * action menu. Edit the sample records below.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-dns-record-manager, [ui-dns-record-manager]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': '\"dns-record-manager\"',\n    '[class]': 'hostClass',\n  },\n  imports: [\n    LucideAngularModule,\n    UiBadgeComponent,\n    UiButtonComponent,\n    UiCardComponent,\n    UiCardContentComponent,\n    UiCardDescriptionComponent,\n    UiCardHeaderComponent,\n    UiCardTitleComponent,\n    UiDropdownMenuComponent,\n    UiDropdownMenuContentComponent,\n    UiDropdownMenuItemComponent,\n    UiDropdownMenuSeparatorComponent,\n    UiDropdownMenuTriggerComponent,\n    UiInputComponent,\n    UiSelectComponent,\n    UiSelectContentComponent,\n    UiSelectItemComponent,\n    UiSelectTriggerComponent,\n    UiSelectValueComponent,\n    UiSeparatorComponent,\n    UiSwitchComponent,\n    UiTableComponent,\n    UiTableBodyComponent,\n    UiTableCellComponent,\n    UiTableHeadComponent,\n    UiTableHeaderComponent,\n    UiTableRowComponent,\n  ],\n  template: `\n    <!-- Domain Header & Status Card -->\n    <div ui-card class=\"border-border bg-card shadow-xs\">\n      <div ui-card-header class=\"pb-4\">\n        <div class=\"flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n          <div class=\"space-y-1.5\">\n            <div class=\"flex flex-wrap items-center gap-2.5\">\n              <div class=\"bg-primary/10 text-primary flex size-8 items-center justify-center rounded-lg\">\n                <lucide-icon [img]=\"Globe\" class=\"size-4.5\" />\n              </div>\n              <h2 class=\"text-foreground text-xl font-bold tracking-tight sm:text-2xl\">{{ domain }}</h2>\n              @if (dnssecEnabled) {\n                <span ui-badge variant=\"outline\" class=\"border-success/30 bg-success/10 text-success gap-1.5 py-0.5\">\n                  <span class=\"bg-success size-1.5 rounded-full\"></span>\n                  <lucide-icon [img]=\"ShieldCheck\" class=\"size-3.5\" />\n                  Active · DNSSEC Enabled\n                </span>\n              } @else {\n                <span ui-badge variant=\"outline\" class=\"border-warning/30 bg-warning/10 text-warning gap-1.5 py-0.5\">\n                  <span class=\"bg-warning size-1.5 rounded-full\"></span>\n                  <lucide-icon [img]=\"ShieldAlert\" class=\"size-3.5\" />\n                  Active · DNSSEC Inactive\n                </span>\n              }\n            </div>\n            <p class=\"text-muted-foreground text-xs sm:text-sm\">\n              Authoritative DNS routing and global edge proxy management for {{ domain }}.\n            </p>\n          </div>\n\n          <!-- Header CTA Buttons -->\n          <div class=\"flex items-center gap-2\">\n            <button\n              ui-button\n              variant=\"outline\"\n              size=\"sm\"\n              class=\"h-8 gap-1.5 text-xs font-medium\"\n              (click)=\"isZoneFileOpen.set(!isZoneFileOpen())\"\n            >\n              <lucide-icon [img]=\"FileCode\" class=\"size-3.5\" />\n              {{ isZoneFileOpen() ? 'Hide Zone File' : 'Export Zone File' }}\n            </button>\n            <button ui-button size=\"sm\" class=\"h-8 gap-1.5 text-xs font-medium\" (click)=\"startAdd()\">\n              <lucide-icon [img]=\"Plus\" class=\"size-3.5\" />\n              Add Record\n            </button>\n          </div>\n        </div>\n\n        <div ui-separator class=\"my-4\"></div>\n\n        <!-- Nameserver Inspector Bar -->\n        <div class=\"flex flex-wrap items-center justify-between gap-3 text-xs\">\n          <div class=\"flex flex-wrap items-center gap-2\">\n            <span class=\"text-muted-foreground font-medium\">Nameservers:</span>\n            <div class=\"flex flex-wrap items-center gap-1.5\">\n              @for (ns of nameservers; track ns) {\n                <button\n                  type=\"button\"\n                  [attr.aria-label]=\"'Copy nameserver ' + ns\"\n                  class=\"border-border bg-muted/60 hover:bg-muted focus-visible:ring-ring/50 group text-foreground flex min-h-6 items-center gap-1.5 rounded-md border px-2.5 py-1 font-mono text-xs transition-colors focus-visible:ring-[2px] focus-visible:outline-none\"\n                  (click)=\"copyText(ns, ns)\"\n                >\n                  <span>{{ ns }}</span>\n                  @if (copiedKey() === ns) {\n                    <lucide-icon [img]=\"Check\" class=\"text-success size-3\" />\n                  } @else {\n                    <lucide-icon [img]=\"Copy\" class=\"text-muted-foreground group-hover:text-foreground size-3\" />\n                  }\n                </button>\n              }\n            </div>\n          </div>\n          <div class=\"text-muted-foreground flex items-center gap-2\">\n            <span class=\"bg-success size-2 animate-pulse rounded-full\"></span>\n            <span>Propagation: 100% Synced</span>\n          </div>\n        </div>\n      </div>\n    </div>\n\n    <!-- Zone File Export Card (Collapsible) -->\n    @if (isZoneFileOpen()) {\n      <div ui-card class=\"border-border bg-card shadow-xs\">\n        <div ui-card-header class=\"pb-3\">\n          <div class=\"flex items-center justify-between\">\n            <div>\n              <div ui-card-title class=\"text-sm font-semibold sm:text-base\">BIND RFC 1035 Zone File</div>\n              <div ui-card-description class=\"text-xs\">\n                Complete DNS zone mapping for {{ domain }} ready for import into Route53, Bind9, or Cloudflare.\n              </div>\n            </div>\n            <button\n              ui-button\n              variant=\"ghost\"\n              size=\"icon-sm\"\n              class=\"size-7\"\n              aria-label=\"Close zone file viewer\"\n              (click)=\"isZoneFileOpen.set(false)\"\n            >\n              <lucide-icon [img]=\"X\" class=\"size-4\" />\n            </button>\n          </div>\n        </div>\n        <div ui-card-content class=\"space-y-3 pt-0\">\n          <div class=\"relative\">\n            <pre\n              class=\"border-border bg-muted/70 text-foreground max-h-56 overflow-x-auto rounded-lg border p-3 font-mono text-xs leading-relaxed select-all\"\n              >{{ zoneFileContent() }}</pre>\n          </div>\n          <div class=\"flex items-center justify-end gap-2\">\n            <button\n              ui-button\n              variant=\"outline\"\n              size=\"sm\"\n              class=\"h-8 gap-1.5 text-xs\"\n              (click)=\"copyText('zone-file', zoneFileContent())\"\n            >\n              @if (copiedKey() === 'zone-file') {\n                <lucide-icon [img]=\"Check\" class=\"text-success size-3.5\" />\n              } @else {\n                <lucide-icon [img]=\"Copy\" class=\"size-3.5\" />\n              }\n              {{ copiedKey() === 'zone-file' ? 'Copied Zone File' : 'Copy Zone Content' }}\n            </button>\n            <button\n              ui-button\n              aria-label=\"Download attachment\"\n              size=\"sm\"\n              class=\"h-8 gap-1.5 text-xs\"\n              (click)=\"downloadZoneFile()\"\n            >\n              <lucide-icon [img]=\"Download\" class=\"size-3.5\" />\n              Download .zone File\n            </button>\n          </div>\n        </div>\n      </div>\n    }\n\n    <!-- Add / Edit DNS Record Card -->\n    @if (isAddOpen()) {\n      <div ui-card class=\"border-primary/30 bg-muted/20 shadow-xs\">\n        <div ui-card-header class=\"pb-4\">\n          <div class=\"flex items-center justify-between\">\n            <div>\n              <div ui-card-title class=\"text-base font-semibold\">\n                {{ editingId() ? 'Edit DNS Record' : 'Add New DNS Record' }}\n              </div>\n              <div ui-card-description class=\"text-xs\">\n                {{\n                  editingId()\n                    ? 'Update configuration, target destination, or edge proxy status.'\n                    : 'Configure host routing, MX exchangers, or verification TXT tags for ' + domain + '.'\n                }}\n              </div>\n            </div>\n            <button\n              ui-button\n              variant=\"ghost\"\n              size=\"icon-sm\"\n              class=\"size-7\"\n              aria-label=\"Cancel record form\"\n              (click)=\"cancelForm()\"\n            >\n              <lucide-icon [img]=\"X\" class=\"size-4\" />\n            </button>\n          </div>\n        </div>\n        <div ui-card-content class=\"space-y-4 pt-0\">\n          <div class=\"grid grid-cols-1 gap-3 sm:grid-cols-12\">\n            <!-- Type Select -->\n            <div class=\"sm:col-span-3\">\n              <label for=\"dns-type\" class=\"text-foreground mb-1.5 block text-xs font-medium\">Type</label>\n              <ui-select [value]=\"formType()\" (valueChange)=\"handleTypeChange($event)\">\n                <button ui-select-trigger id=\"dns-type\" class=\"w-full [&_svg]:shrink-0 [&>span]:truncate\">\n                  <ui-select-value placeholder=\"Record type\" />\n                </button>\n                <ui-select-content>\n                  <ui-select-item value=\"A\">A (IPv4 Address)</ui-select-item>\n                  <ui-select-item value=\"AAAA\">AAAA (IPv6 Address)</ui-select-item>\n                  <ui-select-item value=\"CNAME\">CNAME (Canonical Alias)</ui-select-item>\n                  <ui-select-item value=\"MX\">MX (Mail Exchanger)</ui-select-item>\n                  <ui-select-item value=\"TXT\">TXT (Text Record)</ui-select-item>\n                  <ui-select-item value=\"CAA\">CAA (Cert Authority)</ui-select-item>\n                </ui-select-content>\n              </ui-select>\n            </div>\n\n            <!-- Name Input -->\n            <div class=\"sm:col-span-5\">\n              <label for=\"dns-name\" class=\"text-foreground mb-1.5 block text-xs font-medium\">\n                Name / Subdomain <span class=\"text-muted-foreground font-normal\">(&#64; for root)</span>\n              </label>\n              <ui-input\n                id=\"dns-name\"\n                [value]=\"formName()\"\n                (valueChange)=\"formName.set($event)\"\n                placeholder=\"@ or subdomain\"\n                class=\"font-mono text-xs\"\n              />\n            </div>\n\n            <!-- TTL Select -->\n            <div class=\"sm:col-span-4\">\n              <label for=\"dns-ttl\" class=\"text-foreground mb-1.5 block text-xs font-medium\">TTL</label>\n              <ui-select [value]=\"formTtl()\" (valueChange)=\"formTtl.set($event)\">\n                <button ui-select-trigger id=\"dns-ttl\" class=\"w-full [&_svg]:shrink-0 [&>span]:truncate\">\n                  <ui-select-value placeholder=\"TTL\" />\n                </button>\n                <ui-select-content>\n                  <ui-select-item value=\"Auto\">Auto (Automatic)</ui-select-item>\n                  <ui-select-item value=\"60s\">1 min (60s)</ui-select-item>\n                  <ui-select-item value=\"300s\">5 mins (300s)</ui-select-item>\n                  <ui-select-item value=\"1800s\">30 mins (1800s)</ui-select-item>\n                  <ui-select-item value=\"3600s\">1 hour (3600s)</ui-select-item>\n                  <ui-select-item value=\"86400s\">1 day (86400s)</ui-select-item>\n                </ui-select-content>\n              </ui-select>\n            </div>\n          </div>\n\n          <div [class]=\"formType() === 'MX' ? 'grid grid-cols-1 gap-3 sm:grid-cols-12' : 'grid grid-cols-1 gap-3'\">\n            <!-- Content / Target Input -->\n            <div [class]=\"formType() === 'MX' ? 'sm:col-span-9' : 'w-full'\">\n              <label for=\"dns-content\" class=\"text-foreground mb-1.5 block text-xs font-medium\">\n                {{ contentLabel() }}\n              </label>\n              <ui-input\n                id=\"dns-content\"\n                [value]=\"formContent()\"\n                (valueChange)=\"formContent.set($event)\"\n                [placeholder]=\"contentPlaceholder()\"\n                class=\"font-mono text-xs\"\n              />\n            </div>\n\n            <!-- Priority Input for MX -->\n            @if (formType() === 'MX') {\n              <div class=\"sm:col-span-3\">\n                <label for=\"dns-priority\" class=\"text-foreground mb-1.5 block text-xs font-medium\">Priority</label>\n                <ui-input\n                  id=\"dns-priority\"\n                  type=\"number\"\n                  [min]=\"0\"\n                  [max]=\"65535\"\n                  [value]=\"formPriority()\"\n                  (valueChange)=\"formPriority.set(toNumber($event))\"\n                  placeholder=\"10\"\n                  class=\"font-mono text-xs\"\n                />\n              </div>\n            }\n          </div>\n\n          <!-- Optional Comment -->\n          <div>\n            <label for=\"dns-comment\" class=\"text-foreground mb-1.5 block text-xs font-medium\">\n              Comment <span class=\"text-muted-foreground font-normal\">(optional note)</span>\n            </label>\n            <ui-input\n              id=\"dns-comment\"\n              [value]=\"formComment()\"\n              (valueChange)=\"formComment.set($event)\"\n              placeholder=\"e.g. Production ingress or verification tag\"\n              class=\"text-xs\"\n            />\n          </div>\n\n          <!-- Proxy Switch (for A, AAAA, CNAME) -->\n          @if (isProxyable(formType())) {\n            <div class=\"border-border bg-background/80 flex items-center justify-between rounded-lg border p-3\">\n              <div class=\"space-y-0.5\">\n                <div class=\"flex items-center gap-2\">\n                  <lucide-icon [img]=\"Cloud\" class=\"text-warning size-4\" />\n                  <span class=\"text-foreground text-xs font-medium\">Proxy Status</span>\n                </div>\n                <p class=\"text-muted-foreground text-xs\">\n                  {{\n                    formProxied()\n                      ? 'Proxied: Accelerate traffic and protect origin IP behind Cloudflare edge.'\n                      : 'DNS only: Direct traffic route without edge proxy caching.'\n                  }}\n                </p>\n              </div>\n              <button\n                ui-switch\n                id=\"proxy-toggle\"\n                [checked]=\"formProxied()\"\n                (checkedChange)=\"formProxied.set($event)\"\n              ></button>\n            </div>\n          }\n\n          <!-- Form Actions -->\n          <div class=\"flex items-center justify-end gap-2 pt-2\">\n            <button ui-button variant=\"outline\" size=\"sm\" class=\"h-8 text-xs\" (click)=\"cancelForm()\">Cancel</button>\n            <button ui-button size=\"sm\" class=\"h-8 text-xs\" [disabled]=\"!formContent().trim()\" (click)=\"saveRecord()\">\n              {{ editingId() ? 'Update Record' : 'Save Record' }}\n            </button>\n          </div>\n        </div>\n      </div>\n    }\n\n    <!-- DNS Records Management Section -->\n    <div class=\"space-y-3\">\n      <!-- Toolbar Filter & Search -->\n      <div class=\"flex flex-col gap-2.5 sm:flex-row sm:items-center sm:justify-between\">\n        <div class=\"relative w-full max-w-sm\">\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 records by name, target, or content...\"\n            class=\"h-8.5 pl-8 text-xs\"\n          />\n        </div>\n\n        <!-- Type Filter Buttons -->\n        <div class=\"flex items-center gap-1.5 overflow-x-auto pb-1 sm:pb-0\">\n          <button\n            type=\"button\"\n            [class]=\"filterClass('ALL', 'min-h-6 rounded-md px-2.5 py-1 text-xs font-medium transition-colors')\"\n            (click)=\"selectedTypeFilter.set('ALL')\"\n          >\n            All ({{ records().length }})\n          </button>\n          @for (t of filterTypes; track t) {\n            <button\n              type=\"button\"\n              [class]=\"\n                filterClass(t, 'min-h-6 min-w-6 rounded-md px-2 py-1 font-mono text-xs font-medium transition-colors')\n              \"\n              (click)=\"selectedTypeFilter.set(t)\"\n            >\n              {{ t }}\n            </button>\n          }\n        </div>\n      </div>\n\n      <!-- DNS Records Table -->\n      <div class=\"border-border bg-card overflow-hidden rounded-lg border shadow-xs\">\n        <div class=\"overflow-x-auto\">\n          <div ui-table>\n            <thead ui-table-header>\n              <tr ui-table-row class=\"bg-muted/40\">\n                <th ui-table-head class=\"w-24\">Type</th>\n                <th ui-table-head class=\"min-w-[140px]\">Name</th>\n                <th ui-table-head class=\"min-w-[240px]\">Target Content</th>\n                <th ui-table-head class=\"w-28 text-center\">Proxy Status</th>\n                <th ui-table-head class=\"w-20\">TTL</th>\n                <th ui-table-head class=\"w-12 text-right\">\n                  <span class=\"sr-only\">Actions</span>\n                </th>\n              </tr>\n            </thead>\n            <tbody ui-table-body>\n              @for (record of filteredRecords(); track record.id) {\n                <tr\n                  ui-table-row\n                  [class]=\"\n                    editingId() === record.id\n                      ? 'hover:bg-muted/30 bg-primary/5 transition-colors'\n                      : 'hover:bg-muted/30 transition-colors'\n                  \"\n                >\n                  <!-- Record Type Badge -->\n                  <td ui-table-cell>\n                    <span [class]=\"typeBadgeClass(record.type)\">{{ record.type }}</span>\n                  </td>\n\n                  <!-- Name & Hostname helper -->\n                  <td ui-table-cell>\n                    <div class=\"flex flex-col\">\n                      <span class=\"text-foreground font-mono text-xs font-semibold\">{{ record.name }}</span>\n                      <span class=\"text-muted-foreground truncate font-mono text-xs\">\n                        {{ resolveHostname(record.name) }}\n                      </span>\n                    </div>\n                  </td>\n\n                  <!-- Target Content with Copy button -->\n                  <td ui-table-cell>\n                    <div class=\"flex items-center gap-2\">\n                      <div class=\"min-w-0 flex-1\">\n                        <div class=\"flex items-center gap-1.5\">\n                          @if (record.type === 'MX') {\n                            <span class=\"border-border bg-muted text-muted-foreground rounded px-1 font-mono text-xs\">\n                              Pri {{ record.priority }}\n                            </span>\n                          }\n                          <code class=\"text-foreground/90 font-mono text-xs break-all select-all\">\n                            {{ record.content }}\n                          </code>\n                        </div>\n                        @if (record.comment) {\n                          <p class=\"text-muted-foreground mt-0.5 truncate text-xs\">{{ record.comment }}</p>\n                        }\n                      </div>\n\n                      <button\n                        ui-button\n                        variant=\"ghost\"\n                        size=\"icon-sm\"\n                        class=\"text-muted-foreground hover:text-foreground size-7 shrink-0\"\n                        [attr.aria-label]=\"'Copy content for ' + record.name\"\n                        (click)=\"copyText(record.id, record.content)\"\n                      >\n                        @if (copiedKey() === record.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                  <!-- Proxy Status Switch / Cloud badge -->\n                  <td ui-table-cell class=\"text-center\">\n                    @if (isProxyable(record.type)) {\n                      <div class=\"flex justify-center\">\n                        <button\n                          type=\"button\"\n                          [attr.aria-label]=\"\n                            'Toggle proxy status for ' +\n                            record.name +\n                            ', currently ' +\n                            (record.proxied ? 'proxied' : 'dns only')\n                          \"\n                          [class]=\"proxyButtonClass(record.proxied)\"\n                          (click)=\"toggleProxy(record.id)\"\n                        >\n                          @if (record.proxied) {\n                            <lucide-icon [img]=\"Cloud\" class=\"fill-warning text-warning size-3.5\" />\n                          } @else {\n                            <lucide-icon\n                              [img]=\"CloudOff\"\n                              class=\"text-muted-foreground group-hover:text-foreground size-3.5\"\n                            />\n                          }\n                          <span>{{ record.proxied ? 'Proxied' : 'DNS only' }}</span>\n                        </button>\n                      </div>\n                    } @else {\n                      <div class=\"flex justify-center\">\n                        <span class=\"text-muted-foreground/60 inline-flex items-center gap-1 font-mono text-xs\">\n                          <lucide-icon [img]=\"CloudOff\" class=\"size-3\" />\n                          DNS only\n                        </span>\n                      </div>\n                    }\n                  </td>\n\n                  <!-- TTL Badge -->\n                  <td ui-table-cell>\n                    <span ui-badge variant=\"outline\" class=\"border-border bg-muted/40 font-mono text-xs font-normal\">\n                      {{ record.ttl }}\n                    </span>\n                  </td>\n\n                  <!-- Row Actions Dropdown -->\n                  <td ui-table-cell class=\"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 size-7 p-0\"\n                      >\n                        <lucide-icon [img]=\"MoreHorizontal\" class=\"size-4\" />\n                        <span class=\"sr-only\">Open record actions</span>\n                      </button>\n                      <ui-dropdown-menu-content align=\"end\" class=\"w-44\">\n                        <ui-dropdown-menu-item (select)=\"startEdit(record)\">\n                          <lucide-icon [img]=\"Edit2\" class=\"mr-2 size-3.5\" />\n                          Edit Record\n                        </ui-dropdown-menu-item>\n                        <ui-dropdown-menu-item (select)=\"copyText(record.id, record.content)\">\n                          <lucide-icon [img]=\"Copy\" class=\"mr-2 size-3.5\" />\n                          Copy Target\n                        </ui-dropdown-menu-item>\n                        <ui-dropdown-menu-item (select)=\"copyText(record.id + '-bind', bindRow(record))\">\n                          <lucide-icon [img]=\"FileCode\" class=\"mr-2 size-3.5\" />\n                          Copy BIND Row\n                        </ui-dropdown-menu-item>\n                        <ui-dropdown-menu-separator />\n                        <ui-dropdown-menu-item\n                          class=\"text-destructive focus:text-destructive\"\n                          (select)=\"deleteRecord(record.id)\"\n                        >\n                          <lucide-icon [img]=\"Trash2\" class=\"mr-2 size-3.5\" />\n                          Delete Record\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 (filteredRecords().length === 0) {\n                <tr ui-table-row>\n                  <td ui-table-cell colspan=\"6\" class=\"h-32 text-center\">\n                    <div class=\"flex flex-col items-center justify-center gap-1.5 text-center\">\n                      <lucide-icon [img]=\"Globe\" class=\"text-muted-foreground/50 size-8\" />\n                      <p class=\"text-foreground text-sm font-medium\">No DNS records found</p>\n                      <p class=\"text-muted-foreground max-w-sm text-xs\">\n                        {{\n                          searchQuery()\n                            ? 'No records match your search filter query.'\n                            : 'No records configured for this domain yet. Add your first record to begin routing.'\n                        }}\n                      </p>\n                      <button ui-button size=\"sm\" class=\"mt-2 h-7 gap-1 text-xs\" (click)=\"startAdd()\">\n                        <lucide-icon [img]=\"Plus\" class=\"size-3\" />\n                        Add DNS Record\n                      </button>\n                    </div>\n                  </td>\n                </tr>\n              }\n            </tbody>\n          </div>\n        </div>\n      </div>\n\n      <!-- Table Footer info -->\n      <div class=\"text-muted-foreground flex flex-wrap items-center justify-between gap-2 px-1 text-xs\">\n        <p>\n          Showing <span class=\"text-foreground font-medium\">{{ filteredRecords().length }}</span> of\n          <span class=\"text-foreground font-medium\">{{ records().length }}</span> configured records\n        </p>\n        <div class=\"flex items-center gap-4\">\n          <span class=\"flex items-center gap-1.5\">\n            <span class=\"bg-warning size-2 rounded-full\"></span>\n            {{ proxiedCount() }} Proxied\n          </span>\n          <span class=\"flex items-center gap-1.5\">\n            <span class=\"bg-muted-foreground size-2 rounded-full\"></span>\n            {{ dnsOnlyCount() }} DNS Only\n          </span>\n        </div>\n      </div>\n    </div>\n  `,\n})\nexport class UiDnsRecordManagerComponent implements OnInit, OnDestroy {\n  @Input() domain = 'uipkge.dev'\n  @Input() nameservers: string[] = ['ns1.uipkge.net', 'ns2.uipkge.net']\n  @Input() dnssecEnabled = true\n  @Input() initialRecords?: DnsRecord[]\n  @Input() initialAddOpen = false\n  @Input('class') className?: string\n\n  protected readonly Check = Check\n  protected readonly Cloud = Cloud\n  protected readonly CloudOff = CloudOff\n  protected readonly Copy = Copy\n  protected readonly Download = Download\n  protected readonly Edit2 = Edit2\n  protected readonly FileCode = FileCode\n  protected readonly Globe = Globe\n  protected readonly MoreHorizontal = MoreHorizontal\n  protected readonly Plus = Plus\n  protected readonly Search = Search\n  protected readonly ShieldAlert = ShieldAlert\n  protected readonly ShieldCheck = ShieldCheck\n  protected readonly Trash2 = Trash2\n  protected readonly X = X\n\n  protected readonly filterTypes: DnsRecordType[] = ['A', 'CNAME', 'MX', 'TXT', 'AAAA', 'CAA']\n\n  readonly records = signal<DnsRecord[]>(defaultRecords.map((r) => ({ ...r })))\n  readonly searchQuery = signal('')\n  readonly selectedTypeFilter = signal<string>('ALL')\n  readonly isAddOpen = signal(false)\n  readonly isZoneFileOpen = signal(false)\n  readonly editingId = signal<string | null>(null)\n\n  // Form states\n  readonly formType = signal<DnsRecordType>('A')\n  readonly formName = signal('')\n  readonly formContent = signal('')\n  readonly formTtl = signal('Auto')\n  readonly formProxied = signal(true)\n  readonly formPriority = signal(10)\n  readonly formComment = signal('')\n\n  readonly copiedKey = signal<string | null>(null)\n  private copyTimeout: ReturnType<typeof setTimeout> | undefined\n  // Domain / nameservers are plain inputs; bump this so the zone file recomputes after edits.\n  private readonly inputsVersion = signal(0)\n\n  ngOnInit(): void {\n    this.records.set(\n      this.initialRecords ? this.initialRecords.map((r) => ({ ...r })) : defaultRecords.map((r) => ({ ...r })),\n    )\n    this.isAddOpen.set(this.initialAddOpen)\n    this.inputsVersion.update((v) => v + 1)\n  }\n\n  ngOnDestroy(): void {\n    clearTimeout(this.copyTimeout)\n  }\n\n  get hostClass(): string {\n    return cn('block w-full space-y-6', this.className)\n  }\n\n  readonly filteredRecords = computed(() => {\n    const query = this.searchQuery().trim().toLowerCase()\n    const filter = this.selectedTypeFilter()\n    return this.records().filter((record) => {\n      const matchesType = filter === 'ALL' || record.type === filter\n      const matchesQuery =\n        !query ||\n        record.name.toLowerCase().includes(query) ||\n        record.content.toLowerCase().includes(query) ||\n        record.type.toLowerCase().includes(query) ||\n        (!!record.comment && record.comment.toLowerCase().includes(query))\n      return matchesType && matchesQuery\n    })\n  })\n\n  readonly proxiedCount = computed(() => this.records().filter((r) => r.proxied).length)\n  readonly dnsOnlyCount = computed(() => this.records().filter((r) => !r.proxied).length)\n\n  readonly zoneFileContent = computed(() => {\n    this.inputsVersion()\n    const domain = this.domain\n    const nameservers = this.nameservers\n    const date = new Date().toISOString().split('T')[0]!.replace(/-/g, '')\n    const lines: string[] = [\n      `; BIND zone file for ${domain}`,\n      `; Generated on ${new Date().toUTCString()}`,\n      `$ORIGIN ${domain}.`,\n      `$TTL 3600`,\n      ``,\n      `; SOA Record`,\n      `@       IN  SOA  ${nameservers[0]}. hostmaster.${domain}. (`,\n      `                 ${date}01 ; Serial`,\n      `                 7200       ; Refresh (2 hours)`,\n      `                 3600       ; Retry (1 hour)`,\n      `                 1209600    ; Expire (2 weeks)`,\n      `                 3600       ; Minimum TTL (1 hour)`,\n      `)`,\n      ``,\n      `; Nameservers`,\n    ]\n\n    nameservers.forEach((ns) => {\n      lines.push(`@       IN  NS   ${ns}.`)\n    })\n\n    lines.push('', '; Resource Records')\n\n    for (const r of this.records()) {\n      const recName = (r.name === '@' ? '@' : r.name).padEnd(12, ' ')\n      const ttlValue = r.ttl === 'Auto' ? '3600' : r.ttl.replace('s', '')\n      const ttlStr = ttlValue.padEnd(6, ' ')\n      const typeStr = r.type.padEnd(7, ' ')\n\n      if (r.type === 'MX') {\n        const prio = String(r.priority ?? 10).padEnd(4, ' ')\n        lines.push(`${recName} ${ttlStr} IN  ${typeStr} ${prio} ${r.content}.`)\n      } else if (r.type === 'TXT') {\n        lines.push(`${recName} ${ttlStr} IN  ${typeStr} \"${r.content}\"`)\n      } else if (r.type === 'CNAME') {\n        lines.push(`${recName} ${ttlStr} IN  ${typeStr} ${r.content}.`)\n      } else {\n        lines.push(`${recName} ${ttlStr} IN  ${typeStr} ${r.content}`)\n      }\n    }\n\n    return lines.join('\\n')\n  })\n\n  contentLabel(): string {\n    return contentLabelMap[this.formType()]\n  }\n\n  contentPlaceholder(): string {\n    return placeholderMap[this.formType()]\n  }\n\n  isProxyable(type: DnsRecordType): boolean {\n    return PROXYABLE.includes(type)\n  }\n\n  typeBadgeClass(type: DnsRecordType): string {\n    const s = typeBadgeStyles[type]\n    return cn(\n      'inline-flex items-center justify-center rounded-md border px-2 py-0.5 font-mono text-xs font-bold',\n      s.bg,\n      s.text,\n      s.border,\n    )\n  }\n\n  filterClass(value: string, base: string): string {\n    return cn(\n      base,\n      this.selectedTypeFilter() === value\n        ? 'bg-primary text-primary-foreground shadow-xs'\n        : 'bg-muted/70 text-muted-foreground hover:bg-muted hover:text-foreground',\n    )\n  }\n\n  proxyButtonClass(proxied: boolean): string {\n    return cn(\n      'group inline-flex min-h-6 items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-xs font-medium transition-colors focus-visible:ring-[2px] focus-visible:outline-none',\n      proxied\n        ? 'border-warning/30 bg-warning/10 text-warning hover:bg-warning/20 text-warning'\n        : 'border-border bg-muted/60 text-muted-foreground hover:bg-muted hover:text-foreground',\n    )\n  }\n\n  toNumber(v: string): number {\n    return Number(v)\n  }\n\n  bindRow(record: DnsRecord): string {\n    return `${record.name} IN ${record.type} ${record.content}`\n  }\n\n  resolveHostname(recordName: string): string {\n    if (recordName === '@') return this.domain\n    return `${recordName}.${this.domain}`\n  }\n\n  async copyText(key: string, text: string): Promise<void> {\n    try {\n      await navigator.clipboard.writeText(text)\n      this.copiedKey.set(key)\n      clearTimeout(this.copyTimeout)\n      this.copyTimeout = setTimeout(() => this.copiedKey.set(null), 1600)\n    } catch {\n      // Clipboard unavailable\n    }\n  }\n\n  handleTypeChange(value: string): void {\n    const newType = value as DnsRecordType\n    this.formType.set(newType)\n    if (['MX', 'TXT', 'CAA'].includes(newType)) {\n      this.formProxied.set(false)\n    } else if (!this.editingId()) {\n      this.formProxied.set(true)\n    }\n  }\n\n  toggleProxy(id: string): void {\n    this.records.update((prev) =>\n      prev.map((r) => (r.id === id && this.isProxyable(r.type) ? { ...r, proxied: !r.proxied } : r)),\n    )\n  }\n\n  deleteRecord(id: string): void {\n    this.records.update((prev) => prev.filter((r) => r.id !== id))\n    if (this.editingId() === id) this.cancelForm()\n  }\n\n  startAdd(): void {\n    this.editingId.set(null)\n    this.formType.set('A')\n    this.formName.set('')\n    this.formContent.set('')\n    this.formTtl.set('Auto')\n    this.formProxied.set(true)\n    this.formPriority.set(10)\n    this.formComment.set('')\n    this.isAddOpen.set(true)\n  }\n\n  startEdit(record: DnsRecord): void {\n    this.editingId.set(record.id)\n    this.formType.set(record.type)\n    this.formName.set(record.name)\n    this.formContent.set(record.content)\n    this.formTtl.set(record.ttl)\n    this.formProxied.set(record.proxied)\n    this.formPriority.set(record.priority ?? 10)\n    this.formComment.set(record.comment ?? '')\n    this.isAddOpen.set(true)\n  }\n\n  cancelForm(): void {\n    this.isAddOpen.set(false)\n    this.editingId.set(null)\n  }\n\n  saveRecord(): void {\n    const name = this.formName().trim() || '@'\n    const content = this.formContent().trim()\n    if (!content) return\n    const type = this.formType()\n    const fields = {\n      type,\n      name,\n      content,\n      ttl: this.formTtl(),\n      proxied: this.isProxyable(type) ? this.formProxied() : false,\n      priority: type === 'MX' ? Number(this.formPriority()) : undefined,\n      comment: this.formComment().trim() || undefined,\n    }\n\n    const editingId = this.editingId()\n    if (editingId) {\n      this.records.update((prev) => prev.map((r) => (r.id === editingId ? { ...r, ...fields } : r)))\n    } else {\n      this.records.update((prev) => [{ id: `rec-${Date.now()}`, ...fields }, ...prev])\n    }\n\n    this.cancelForm()\n  }\n\n  downloadZoneFile(): void {\n    const blob = new Blob([this.zoneFileContent()], { 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 = `${this.domain}.zone`\n    document.body.appendChild(a)\n    a.click()\n    document.body.removeChild(a)\n    URL.revokeObjectURL(url)\n  }\n}\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/dns-record-manager/dns-record-manager.component.ts"
    },
    {
      "path": "packages/registry-angular/blocks/dns-record-manager/index.ts",
      "content": "export { UiDnsRecordManagerComponent } from './dns-record-manager.component'\nexport type { DnsRecord, DnsRecordManagerProps, DnsRecordType } from './dns-record-manager.component'\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/dns-record-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/dropdown-menu.json",
    "https://uipkge.dev/r/angular/input.json",
    "https://uipkge.dev/r/angular/select.json",
    "https://uipkge.dev/r/angular/separator.json",
    "https://uipkge.dev/r/angular/switch.json",
    "https://uipkge.dev/r/angular/table.json"
  ],
  "description": "Cloudflare/Vercel style DNS records management table and nameserver inspector with type badges, proxy toggle, copyable values, zone file exporter, and add/edit record panel.",
  "categories": [
    "devops",
    "dashboard",
    "data"
  ]
}