{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "api-keys",
  "title": "Api Keys",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-vue/blocks/api-keys/ApiKeys.vue",
      "content": "<script setup lang=\"ts\">\nimport { computed, ref } from 'vue'\nimport { Check, Copy, Eye, EyeOff, KeyRound, Plus } from 'lucide-vue-next'\nimport { cn } from '@/lib/utils'\nimport { SectionCard } from '@/components/ui/section-card'\nimport { Button } from '@/components/ui/button'\nimport { Badge } from '@/components/ui/badge'\nimport { Input } from '@/components/ui/input'\nimport { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogTitle,\n} from '@/components/ui/dialog'\n\ntype ApiKeyEnvironment = 'production' | 'staging' | 'development'\n\ntype ApiKeyStatus = 'active' | 'revoked' | 'expiring'\n\ninterface ApiKey {\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 props = withDefaults(\n  defineProps<{\n    /** Replacement seed data. Pass [] to start from the empty state. */\n    initialKeys?: ApiKey[]\n    initialCreateOpen?: boolean\n    density?: 'default' | 'compact'\n    class?: string\n  }>(),\n  {\n    initialKeys: undefined,\n    initialCreateOpen: false,\n    density: 'default',\n  },\n)\n\nconst now = new Date()\n\nfunction daysAgo(days: number): Date {\n  return new Date(now.getTime() - days * 24 * 60 * 60 * 1000)\n}\n\nfunction hoursAgo(hours: number): Date {\n  return new Date(now.getTime() - hours * 60 * 60 * 1000)\n}\n\nconst stubKeys: ApiKey[] = [\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\nconst keys = ref<ApiKey[]>(\n  props.initialKeys ? props.initialKeys.map((k) => ({ ...k })) : stubKeys.map((k) => ({ ...k })),\n)\n\nconst dense = computed(() => props.density === 'compact')\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\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 lastUsedText(d: Date | null): string {\n  if (!d) return 'Never'\n  const minutes = Math.max(1, Math.round((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\nconst revealed = ref<Record<string, boolean>>({})\n\nfunction toggleRevealed(id: string) {\n  revealed.value[id] = !revealed.value[id]\n}\n\nconst copiedId = ref<string | null>(null)\nlet copyTimer: number | undefined\n\nasync function copyKey(key: ApiKey) {\n  try {\n    await navigator.clipboard.writeText(key.value)\n    copiedId.value = key.id\n    window.clearTimeout(copyTimer)\n    copyTimer = window.setTimeout(() => {\n      copiedId.value = null\n    }, 1600)\n  } catch {\n    // Clipboard unavailable (e.g. insecure context) — skip the feedback swap.\n  }\n}\n\nconst confirmingId = ref<string | null>(null)\nlet confirmTimer: number | undefined\n\nfunction requestRevoke(id: string) {\n  if (confirmingId.value === id) {\n    revokeKey(id)\n    return\n  }\n  confirmingId.value = id\n  window.clearTimeout(confirmTimer)\n  confirmTimer = window.setTimeout(() => {\n    confirmingId.value = null\n  }, 3000)\n}\n\nfunction cancelRevoke(id: string) {\n  if (confirmingId.value === id) {\n    window.clearTimeout(confirmTimer)\n    confirmingId.value = null\n  }\n}\n\nfunction revokeKey(id: string) {\n  window.clearTimeout(confirmTimer)\n  confirmingId.value = null\n  const key = keys.value.find((k) => k.id === id)\n  if (key) key.status = 'revoked'\n}\n\nconst createOpen = ref(props.initialCreateOpen)\nconst newName = ref('')\nconst newEnv = ref<ApiKeyEnvironment>('production')\n\nfunction setCreateOpen(open: boolean) {\n  createOpen.value = open\n  if (open) {\n    newName.value = ''\n    newEnv.value = 'production'\n  }\n}\n\nfunction createKey() {\n  const name = newName.value.trim()\n  if (!name) return\n  const prefix = { production: 'uipkge_live_', staging: 'sk_stg_', development: 'sk_dev_' }[newEnv.value]\n  const random = Array.from({ length: 2 }, () => Math.random().toString(36).slice(2, 14)).join('')\n  keys.value.unshift({\n    id: `key-${Date.now()}`,\n    name,\n    environment: newEnv.value,\n    value: `${prefix}${random}`,\n    createdAt: new Date(),\n    lastUsedAt: null,\n    status: 'active',\n  })\n  setCreateOpen(false)\n}\n</script>\n\n<template>\n  <div data-slot=\"api-keys\" :class=\"cn('w-full', props.class)\">\n    <SectionCard title=\"API Keys\" description=\"Secret keys used to authenticate requests to your API.\">\n      <template #header-action>\n        <Button size=\"sm\" @click=\"setCreateOpen(true)\">\n          <Plus class=\"size-4\" />\n          Create key\n        </Button>\n      </template>\n\n      <div v-if=\"keys.length === 0\" 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          <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 size=\"sm\" class=\"mt-4\" @click=\"setCreateOpen(true)\">Create your first key</Button>\n      </div>\n\n      <ul v-else class=\"-my-4 divide-y\">\n        <li\n          v-for=\"key in keys\"\n          :key=\"key.id\"\n          class=\"flex items-center gap-3 sm:gap-4\"\n          :class=\"dense ? 'py-2.5' : 'py-4'\"\n        >\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              <Badge :variant=\"statusMeta[key.status].variant\">{{ statusMeta[key.status].label }}</Badge>\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            {{ revealed[key.id] ? key.value : maskKeyValue(key.value) }}\n          </code>\n\n          <div class=\"flex shrink-0 items-center gap-1\">\n            <Button\n              variant=\"ghost\"\n              size=\"icon-sm\"\n              :aria-label=\"revealed[key.id] ? 'Hide key value' : 'Reveal key value'\"\n              @click=\"toggleRevealed(key.id)\"\n            >\n              <EyeOff v-if=\"revealed[key.id]\" class=\"size-4\" />\n              <Eye v-else class=\"size-4\" />\n            </Button>\n            <Button\n              variant=\"ghost\"\n              size=\"icon-sm\"\n              :aria-label=\"copiedId === key.id ? 'Copied' : 'Copy key value'\"\n              @click=\"copyKey(key)\"\n            >\n              <Check v-if=\"copiedId === key.id\" class=\"text-success size-4\" />\n              <Copy v-else class=\"text-muted-foreground size-4\" />\n            </Button>\n            <Button\n              v-if=\"key.status !== 'revoked'\"\n              size=\"sm\"\n              :variant=\"confirmingId === key.id ? 'destructive' : 'ghost'\"\n              :class=\"confirmingId === key.id ? '' : '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          </div>\n        </li>\n      </ul>\n    </SectionCard>\n\n    <Dialog :open=\"createOpen\" @update:open=\"setCreateOpen\">\n      <DialogContent class=\"sm:max-w-md\">\n        <DialogHeader>\n          <DialogTitle>Create API key</DialogTitle>\n          <DialogDescription>\n            Generate a new secret key. The full value is only shown once — store it somewhere safe.\n          </DialogDescription>\n        </DialogHeader>\n        <div class=\"grid gap-4 py-2\">\n          <div class=\"grid gap-1.5\">\n            <label for=\"api-key-name\" class=\"text-sm leading-none font-medium\">Name</label>\n            <Input id=\"api-key-name\" v-model=\"newName\" placeholder=\"e.g. Production server\" @keyup.enter=\"createKey\" />\n          </div>\n          <div class=\"grid gap-1.5\">\n            <label for=\"api-key-environment\" class=\"text-sm leading-none font-medium\">Environment</label>\n            <Select v-model=\"newEnv\">\n              <SelectTrigger id=\"api-key-environment\" class=\"w-full\">\n                <SelectValue placeholder=\"Choose an environment\" />\n              </SelectTrigger>\n              <SelectContent>\n                <SelectItem value=\"production\">Production</SelectItem>\n                <SelectItem value=\"staging\">Staging</SelectItem>\n                <SelectItem value=\"development\">Development</SelectItem>\n              </SelectContent>\n            </Select>\n          </div>\n        </div>\n        <DialogFooter>\n          <Button variant=\"outline\" @click=\"setCreateOpen(false)\">Cancel</Button>\n          <Button :disabled=\"!newName.trim()\" @click=\"createKey\">Create key</Button>\n        </DialogFooter>\n      </DialogContent>\n    </Dialog>\n  </div>\n</template>\n",
      "type": "registry:block",
      "target": "~/app/components/blocks/ApiKeys.vue"
    }
  ],
  "dependencies": [
    "lucide-vue-next"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/vue/section-card.json",
    "https://uipkge.dev/r/vue/button.json",
    "https://uipkge.dev/r/vue/dialog.json",
    "https://uipkge.dev/r/vue/input.json",
    "https://uipkge.dev/r/vue/select.json",
    "https://uipkge.dev/r/vue/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"
  ]
}