{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "api-keys",
  "title": "Api Keys",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-angular/blocks/api-keys/api-keys.component.ts",
      "content": "// Developer API-key manager in a SectionCard: self-contained stateful block with stub keys,\n// reveal/copy/revoke row actions, an empty state, and a create-key dialog.\n// Port of the React block 1:1.\nimport { ChangeDetectionStrategy, Component, DestroyRef, Input, inject, signal } from '@angular/core'\nimport { Check, Copy, Eye, EyeOff, KeyRound, LucideAngularModule, Plus } from 'lucide-angular'\nimport { cn } from '@/lib/utils'\nimport { UiBadgeComponent } from '@/ui/badge/badge.component'\nimport { UiButtonComponent } from '@/ui/button/button.component'\nimport {\n  UiDialogComponent,\n  UiDialogContentComponent,\n  UiDialogDescriptionComponent,\n  UiDialogFooterComponent,\n  UiDialogHeaderComponent,\n  UiDialogTitleComponent,\n} from '@/ui/dialog/dialog.component'\nimport { UiInputComponent } from '@/ui/input/input.component'\nimport { UiSectionCardComponent } from '@/ui/section-card/section-card.component'\nimport {\n  UiSelectComponent,\n  UiSelectContentComponent,\n  UiSelectItemComponent,\n  UiSelectTriggerComponent,\n  UiSelectValueComponent,\n} from '@/ui/select/select.component'\n\nexport type ApiKeyEnvironment = 'production' | 'staging' | 'development'\nexport type ApiKeyStatus = 'active' | 'revoked' | 'expiring'\nexport type ApiKeysDensity = 'default' | 'compact'\n\nexport interface ApiKeyItem {\n  id: string\n  name: string\n  environment: ApiKeyEnvironment\n  value: string\n  createdAt: Date\n  lastUsedAt: Date | null\n  status: ApiKeyStatus\n}\n\nconst statusMeta: Record<ApiKeyStatus, { label: string; variant: 'success' | 'warning' | 'destructive' }> = {\n  active: { label: 'Active', variant: 'success' },\n  expiring: { label: 'Expires soon', variant: 'warning' },\n  revoked: { label: 'Revoked', variant: 'destructive' },\n}\n\nconst environmentPrefix: Record<ApiKeyEnvironment, string> = {\n  production: 'uipkge_live_',\n  staging: 'sk_stg_',\n  development: 'sk_dev_',\n}\n\nfunction maskKeyValue(value: string): string {\n  const parts = value.split('_')\n  const prefix = parts.length > 2 ? `${parts[0]}_${parts[1]}_` : ''\n  return `${prefix}••••••••${value.slice(-4)}`\n}\n\nfunction formatDate(d: Date): string {\n  return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })\n}\n\nfunction createStubKeys(now: Date): ApiKeyItem[] {\n  const daysAgo = (days: number) => new Date(now.getTime() - days * 24 * 60 * 60 * 1000)\n  const hoursAgo = (hours: number) => new Date(now.getTime() - hours * 60 * 60 * 1000)\n  return [\n    {\n      id: 'key-prod-server',\n      name: 'Production server',\n      environment: 'production',\n      value: 'uipkge_live_51HxQp9mZvKYlo2C4f2a',\n      createdAt: daysAgo(94),\n      lastUsedAt: hoursAgo(2),\n      status: 'active',\n    },\n    {\n      id: 'key-ci-pipeline',\n      name: 'CI pipeline (GitHub Actions)',\n      environment: 'staging',\n      value: 'sk_stg_6JdRw3nAwLZmp7xQb91e',\n      createdAt: daysAgo(41),\n      lastUsedAt: hoursAgo(26),\n      status: 'active',\n    },\n    {\n      id: 'key-mobile-backend',\n      name: 'Mobile app backend',\n      environment: 'production',\n      value: 'uipkge_live_8KfSx1oCyUZiqw9Ma2b7',\n      createdAt: daysAgo(27),\n      lastUsedAt: hoursAgo(5),\n      status: 'expiring',\n    },\n    {\n      id: 'key-local-dev',\n      name: 'Local development',\n      environment: 'development',\n      value: 'sk_dev_k2MnQ8tReFvBgy5Xc40d',\n      createdAt: daysAgo(12),\n      lastUsedAt: daysAgo(3),\n      status: 'active',\n    },\n    {\n      id: 'key-legacy-webhooks',\n      name: 'Legacy webhook integration',\n      environment: 'production',\n      value: 'uipkge_live_2PvBgR6nTxAokdJe5f8c',\n      createdAt: daysAgo(210),\n      lastUsedAt: daysAgo(64),\n      status: 'revoked',\n    },\n  ]\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-api-keys, [ui-api-keys]',\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    UiDialogComponent,\n    UiDialogContentComponent,\n    UiDialogDescriptionComponent,\n    UiDialogFooterComponent,\n    UiDialogHeaderComponent,\n    UiDialogTitleComponent,\n    UiInputComponent,\n    UiSectionCardComponent,\n    UiSelectComponent,\n    UiSelectContentComponent,\n    UiSelectItemComponent,\n    UiSelectTriggerComponent,\n    UiSelectValueComponent,\n  ],\n  template: `\n    <div data-slot=\"api-keys\" [class]=\"rootClass\">\n      <div ui-section-card title=\"API Keys\" description=\"Secret keys used to authenticate requests to your API.\">\n        <button ui-button size=\"sm\" slot=\"header-action\" (click)=\"openCreate()\">\n          <lucide-icon [img]=\"Plus\" class=\"size-4\" />\n          Create key\n        </button>\n\n        @if (keys().length === 0) {\n          <div class=\"flex flex-col items-center justify-center px-6 py-14 text-center\">\n            <div class=\"bg-muted text-muted-foreground mx-auto grid size-12 place-items-center rounded-full\">\n              <lucide-icon [img]=\"KeyRound\" class=\"size-5\" />\n            </div>\n            <p class=\"mt-3 text-sm font-medium\">No API keys yet</p>\n            <p class=\"text-muted-foreground mt-0.5 max-w-xs text-xs\">\n              Create a key to start authenticating requests against your API.\n            </p>\n            <button ui-button size=\"sm\" class=\"mt-4\" (click)=\"openCreate()\">Create your first key</button>\n          </div>\n        } @else {\n          <ul class=\"-my-4 divide-y\">\n            @for (key of keys(); track key.id) {\n              <li [class]=\"rowClass\">\n                <div class=\"min-w-0 flex-1\">\n                  <div class=\"flex items-center gap-2\">\n                    <p class=\"truncate text-sm font-medium\">{{ key.name }}</p>\n                    <span ui-badge [variant]=\"statusVariant(key.status)\">{{ statusLabel(key.status) }}</span>\n                  </div>\n                  <p class=\"text-muted-foreground mt-0.5 truncate text-xs\">\n                    Created {{ formatDate(key.createdAt) }} · Last used {{ lastUsedText(key.lastUsedAt) }}\n                  </p>\n                </div>\n\n                <code class=\"bg-muted hidden shrink-0 rounded px-2 py-1 font-mono text-xs md:block\">\n                  {{ isRevealed(key.id) ? key.value : mask(key.value) }}\n                </code>\n\n                <div class=\"flex shrink-0 items-center gap-1\">\n                  <button\n                    ui-button\n                    variant=\"ghost\"\n                    size=\"icon-sm\"\n                    [attr.aria-label]=\"isRevealed(key.id) ? 'Hide key value' : 'Reveal key value'\"\n                    (click)=\"toggleRevealed(key.id)\"\n                  >\n                    @if (isRevealed(key.id)) {\n                      <lucide-icon [img]=\"EyeOff\" class=\"size-4\" />\n                    } @else {\n                      <lucide-icon [img]=\"Eye\" class=\"size-4\" />\n                    }\n                  </button>\n                  <button\n                    ui-button\n                    variant=\"ghost\"\n                    size=\"icon-sm\"\n                    [attr.aria-label]=\"copiedId() === key.id ? 'Copied' : 'Copy key value'\"\n                    (click)=\"copyKey(key)\"\n                  >\n                    @if (copiedId() === key.id) {\n                      <lucide-icon [img]=\"Check\" class=\"text-success size-4\" />\n                    } @else {\n                      <lucide-icon [img]=\"Copy\" class=\"text-muted-foreground size-4\" />\n                    }\n                  </button>\n                  @if (key.status !== 'revoked') {\n                    <button\n                      ui-button\n                      size=\"sm\"\n                      [variant]=\"confirmingId() === key.id ? 'destructive' : 'ghost'\"\n                      [class]=\"confirmingId() === key.id ? undefined : 'text-muted-foreground hover:text-destructive'\"\n                      (click)=\"requestRevoke(key.id)\"\n                      (blur)=\"cancelRevoke(key.id)\"\n                    >\n                      {{ confirmingId() === key.id ? 'Confirm?' : 'Revoke' }}\n                    </button>\n                  }\n                </div>\n              </li>\n            }\n          </ul>\n        }\n      </div>\n\n      <ui-dialog [open]=\"createOpen()\" (openChange)=\"handleCreateOpenChange($event)\">\n        <ui-dialog-content class=\"sm:max-w-md\">\n          <div ui-dialog-header>\n            <h2 ui-dialog-title>Create API key</h2>\n            <p ui-dialog-description>\n              Generate a new secret key. The full value is only shown once — store it somewhere safe.\n            </p>\n          </div>\n          <div class=\"grid gap-4 py-2\">\n            <div class=\"grid gap-1.5\">\n              <label for=\"angular-api-key-name\" class=\"text-sm leading-none font-medium\">Name</label>\n              <ui-input\n                id=\"angular-api-key-name\"\n                [value]=\"newName()\"\n                (valueChange)=\"newName.set($event)\"\n                placeholder=\"e.g. Production server\"\n                (keydown.enter)=\"createKey()\"\n              />\n            </div>\n            <div class=\"grid gap-1.5\">\n              <label for=\"angular-api-key-environment\" class=\"text-sm leading-none font-medium\">Environment</label>\n              <ui-select [value]=\"newEnv()\" (valueChange)=\"setNewEnv($event)\">\n                <button ui-select-trigger id=\"angular-api-key-environment\" class=\"w-full\">\n                  <ui-select-value placeholder=\"Choose an environment\" />\n                </button>\n                <ui-select-content>\n                  <ui-select-item value=\"production\">Production</ui-select-item>\n                  <ui-select-item value=\"staging\">Staging</ui-select-item>\n                  <ui-select-item value=\"development\">Development</ui-select-item>\n                </ui-select-content>\n              </ui-select>\n            </div>\n          </div>\n          <div ui-dialog-footer>\n            <button ui-button variant=\"outline\" (click)=\"createOpen.set(false)\">Cancel</button>\n            <button ui-button [disabled]=\"!newName().trim()\" (click)=\"createKey()\">Create key</button>\n          </div>\n        </ui-dialog-content>\n      </ui-dialog>\n    </div>\n  `,\n})\nexport class UiApiKeysComponent {\n  protected readonly Check = Check\n  protected readonly Copy = Copy\n  protected readonly Eye = Eye\n  protected readonly EyeOff = EyeOff\n  protected readonly KeyRound = KeyRound\n  protected readonly Plus = Plus\n\n  private readonly now = new Date()\n\n  @Input() set initialKeys(v: ApiKeyItem[] | undefined) {\n    this.keys.set(v ? v.map((k) => ({ ...k })) : createStubKeys(this.now))\n  }\n  @Input() set initialCreateOpen(v: boolean) {\n    this.createOpen.set(!!v)\n  }\n  @Input() set density(v: ApiKeysDensity) {\n    this.densityValue.set(v ?? 'default')\n  }\n  @Input('class') className?: string\n\n  readonly keys = signal<ApiKeyItem[]>(createStubKeys(this.now))\n  readonly revealed = signal<Record<string, boolean>>({})\n  readonly copiedId = signal<string | null>(null)\n  readonly confirmingId = signal<string | null>(null)\n  readonly createOpen = signal(false)\n  readonly newName = signal('')\n  readonly newEnv = signal<ApiKeyEnvironment>('production')\n  readonly densityValue = signal<ApiKeysDensity>('default')\n\n  private copyTimer: ReturnType<typeof setTimeout> | undefined\n  private confirmTimer: ReturnType<typeof setTimeout> | undefined\n\n  constructor() {\n    inject(DestroyRef).onDestroy(() => {\n      clearTimeout(this.copyTimer)\n      clearTimeout(this.confirmTimer)\n    })\n  }\n\n  get rootClass(): string {\n    return cn('w-full', this.className)\n  }\n\n  get rowClass(): string {\n    return cn('flex items-center gap-3 sm:gap-4', this.densityValue() === 'compact' ? 'py-2.5' : 'py-4')\n  }\n\n  statusLabel(status: ApiKeyStatus): string {\n    return statusMeta[status].label\n  }\n\n  statusVariant(status: ApiKeyStatus): 'success' | 'warning' | 'destructive' {\n    return statusMeta[status].variant\n  }\n\n  mask(value: string): string {\n    return maskKeyValue(value)\n  }\n\n  formatDate(d: Date): string {\n    return formatDate(d)\n  }\n\n  isRevealed(id: string): boolean {\n    return Boolean(this.revealed()[id])\n  }\n\n  lastUsedText(d: Date | null): string {\n    if (!d) return 'Never'\n    const minutes = Math.max(1, Math.round((this.now.getTime() - d.getTime()) / 60000))\n    if (minutes < 60) return `${minutes}m ago`\n    const hours = Math.round(minutes / 60)\n    if (hours < 24) return `${hours}h ago`\n    return `${Math.round(hours / 24)}d ago`\n  }\n\n  setNewEnv(v: string): void {\n    this.newEnv.set(v as ApiKeyEnvironment)\n  }\n\n  toggleRevealed(id: string): void {\n    this.revealed.update((r) => ({ ...r, [id]: !r[id] }))\n  }\n\n  async copyKey(key: ApiKeyItem): Promise<void> {\n    try {\n      await navigator.clipboard.writeText(key.value)\n      this.copiedId.set(key.id)\n      clearTimeout(this.copyTimer)\n      this.copyTimer = setTimeout(() => this.copiedId.set(null), 1600)\n    } catch {\n      // Clipboard unavailable (e.g. insecure context) — skip the feedback swap.\n    }\n  }\n\n  requestRevoke(id: string): void {\n    if (this.confirmingId() === id) {\n      this.revokeKey(id)\n      return\n    }\n    this.confirmingId.set(id)\n    clearTimeout(this.confirmTimer)\n    this.confirmTimer = setTimeout(() => this.confirmingId.set(null), 3000)\n  }\n\n  cancelRevoke(id: string): void {\n    if (this.confirmingId() === id) {\n      clearTimeout(this.confirmTimer)\n      this.confirmingId.set(null)\n    }\n  }\n\n  revokeKey(id: string): void {\n    clearTimeout(this.confirmTimer)\n    this.confirmingId.set(null)\n    this.keys.update((ks) => ks.map((k) => (k.id === id ? { ...k, status: 'revoked' as const } : k)))\n  }\n\n  openCreate(): void {\n    this.newName.set('')\n    this.newEnv.set('production')\n    this.createOpen.set(true)\n  }\n\n  handleCreateOpenChange(open: boolean): void {\n    if (open) this.openCreate()\n    else this.createOpen.set(false)\n  }\n\n  createKey(): void {\n    const name = this.newName().trim()\n    if (!name) return\n    const random = Array.from({ length: 2 }, () => Math.random().toString(36).slice(2, 14)).join('')\n    const env = this.newEnv()\n    this.keys.update((ks) => [\n      {\n        id: `key-${Date.now()}`,\n        name,\n        environment: env,\n        value: `${environmentPrefix[env]}${random}`,\n        createdAt: new Date(),\n        lastUsedAt: null,\n        status: 'active',\n      },\n      ...ks,\n    ])\n    this.createOpen.set(false)\n  }\n}\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/api-keys/api-keys.component.ts"
    },
    {
      "path": "packages/registry-angular/blocks/api-keys/index.ts",
      "content": "export {\n  UiApiKeysComponent,\n  type ApiKeyEnvironment,\n  type ApiKeyItem,\n  type ApiKeysDensity,\n  type ApiKeyStatus,\n} from './api-keys.component'\n",
      "type": "registry:block",
      "target": "~/src/app/components/blocks/api-keys/index.ts"
    }
  ],
  "dependencies": [
    "lucide-angular"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/section-card.json",
    "https://uipkge.dev/r/angular/button.json",
    "https://uipkge.dev/r/angular/dialog.json",
    "https://uipkge.dev/r/angular/input.json",
    "https://uipkge.dev/r/angular/select.json",
    "https://uipkge.dev/r/angular/badge.json"
  ],
  "description": "Developer API-key manager in a SectionCard. Self-contained stateful block with stub keys, reveal/copy/revoke row actions, an empty state, and a create-key dialog.",
  "categories": [
    "security",
    "dashboard",
    "data"
  ]
}