{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "api-key-list",
  "title": "ApiKeyList",
  "description": "Create, reveal-once, and revoke Neon API keys (personal and organization), with an inline create flow, a one-time secret reveal, and per-row revoke confirmation.",
  "dependencies": [
    "@hugeicons/react",
    "@hugeicons/core-free-icons"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.neon.com/r/button.json",
    "https://ui.neon.com/r/input.json",
    "https://ui.neon.com/r/tooltip.json",
    "https://ui.neon.com/r/skeleton.json",
    "https://ui.neon.com/r/neon-tokens.json",
    "https://ui.neon.com/r/select.json"
  ],
  "files": [
    {
      "path": "src/components/api-key-list/api-key-list.tsx",
      "content": "\"use client\";\n\nimport {\n  Alert02Icon,\n  Building01Icon,\n  Copy01Icon,\n  Delete02Icon,\n  Key01Icon,\n  PlusSignIcon,\n  Tick02Icon,\n} from \"@hugeicons/core-free-icons\";\nimport { HugeiconsIcon } from \"@hugeicons/react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport type { ComponentProps, FormEvent, ReactNode } from \"react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from \"@/components/ui/select\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\";\nimport { cn } from \"@/lib/utils\";\n\nconst COPY_FLASH_MS = 1500;\n\n/** Where the key lives: a personal account key or an organization key. */\nexport type ApiKeyScope = \"personal\" | \"organization\";\n\nexport interface ApiKey {\n  id: string;\n  name: string;\n  scope: ApiKeyScope;\n  /** Preformatted creation date, e.g. \"Jul 12, 2026\". */\n  createdAt: string;\n  /** Preformatted last use, e.g. \"2h ago\". Omit for a never-used key. */\n  lastUsedAt?: string;\n}\n\nconst SCOPE_ICON = {\n  organization: Building01Icon,\n  personal: Key01Icon,\n} as const;\n\nconst SCOPE_SUBLABEL: Record<ApiKeyScope, string> = {\n  organization: \"Organization\",\n  personal: \"Personal\",\n};\n\nconst SCOPES: { value: ApiKeyScope; label: string }[] = [\n  { label: \"personal\", value: \"personal\" },\n  { label: \"org\", value: \"organization\" },\n];\n\n/* ─────────────────────────────────────────────────────────\n * A copy that carries the real token and flashes a primary\n * check, announcing through a polite live region. The\n * \"Copied\" note masks the value under a gradient so the row\n * never breaks.\n * ───────────────────────────────────────────────────────── */\nconst CopyButton = ({ value, label }: { value: string; label: string }) => {\n  const [copied, setCopied] = useState(false);\n\n  const copy = async () => {\n    setCopied(true);\n    window.setTimeout(() => setCopied(false), COPY_FLASH_MS);\n\n    try {\n      await navigator.clipboard.writeText(value);\n    } catch {\n      // Clipboard unavailable (blur, permissions); the feedback still shows.\n    }\n  };\n\n  return (\n    <div className=\"relative flex shrink-0 items-center\">\n      <span\n        aria-hidden=\"true\"\n        className={cn(\n          \"pointer-events-none absolute inset-y-0 right-full flex items-center bg-gradient-to-r from-transparent via-card to-card pr-2 pl-8 font-mono text-[10px] text-primary transition-all duration-200 ease-out motion-reduce:transition-none\",\n          copied ? \"translate-x-0 opacity-100\" : \"translate-x-1 opacity-0\"\n        )}\n      >\n        Copied\n      </span>\n      <Tooltip>\n        <TooltipTrigger\n          render={\n            <Button\n              aria-label={label}\n              onClick={copy}\n              size=\"icon-xs\"\n              type=\"button\"\n              variant=\"ghost\"\n            >\n              <HugeiconsIcon\n                className={cn(\"transition-colors\", copied && \"text-primary\")}\n                icon={copied ? Tick02Icon : Copy01Icon}\n                strokeWidth={2}\n              />\n            </Button>\n          }\n        />\n        <TooltipContent>{label}</TooltipContent>\n      </Tooltip>\n      <span aria-live=\"polite\" className=\"sr-only\">\n        {copied ? `${label} copied` : \"\"}\n      </span>\n    </div>\n  );\n};\n\nconst ScopeToggle = ({\n  scope,\n  onSelect,\n}: {\n  scope: ApiKeyScope;\n  onSelect: (scope: ApiKeyScope) => void;\n}) => (\n  <fieldset className=\"m-0 inline-flex shrink-0 rounded-md border border-border/60 p-0.5\">\n    <legend className=\"sr-only\">Key scope</legend>\n    {SCOPES.map((entry) => (\n      <button\n        aria-pressed={scope === entry.value}\n        className={cn(\n          \"rounded-[calc(var(--radius-md)-3px)] px-2 py-0.5 font-mono text-[11px] transition-colors\",\n          scope === entry.value\n            ? \"bg-primary/10 font-medium text-primary\"\n            : \"text-muted-foreground hover:text-foreground\"\n        )}\n        key={entry.value}\n        onClick={() => onSelect(entry.value)}\n        type=\"button\"\n      >\n        {entry.label}\n      </button>\n    ))}\n  </fieldset>\n);\n\ntype ScopeFilterValue = ApiKeyScope | \"all\";\n\nconst ScopeFilter = ({\n  value,\n  onChange,\n}: {\n  value: ScopeFilterValue;\n  onChange: (value: ScopeFilterValue) => void;\n}) => (\n  <Select\n    onValueChange={(next) => {\n      if (next !== null) {\n        onChange(next as ScopeFilterValue);\n      }\n    }}\n    value={value}\n  >\n    <SelectTrigger\n      aria-label=\"Filter by scope\"\n      className=\"h-7 rounded-md border-border/60 bg-muted/20 font-mono text-xs transition-colors hover:border-border hover:bg-muted/40\"\n      size=\"sm\"\n    >\n      <SelectValue />\n    </SelectTrigger>\n    <SelectContent>\n      <SelectItem className=\"font-mono text-xs\" value=\"all\">\n        all\n      </SelectItem>\n      <SelectItem className=\"font-mono text-xs\" value=\"personal\">\n        personal\n      </SelectItem>\n      <SelectItem className=\"font-mono text-xs\" value=\"organization\">\n        org\n      </SelectItem>\n    </SelectContent>\n  </Select>\n);\n\n/* The one-time reveal: the full token, a copy, and a warning that it\n * will never be shown again. Recessed well so it reads as a secret. */\nconst RevealOncePanel = ({\n  name,\n  secret,\n  onDone,\n}: {\n  name: string;\n  secret: string;\n  onDone: () => void;\n}) => (\n  <div\n    className=\"fade-in-0 slide-in-from-top-1 animate-in space-y-2 rounded-md border border-primary/30 bg-background p-3 duration-200 motion-reduce:animate-none\"\n    data-slot=\"api-key-reveal\"\n  >\n    <div className=\"flex items-center gap-2 text-primary\">\n      <HugeiconsIcon className=\"size-3.5\" icon={Tick02Icon} strokeWidth={2} />\n      <p className=\"min-w-0 truncate font-medium text-xs\">{name} created</p>\n    </div>\n    <div className=\"flex min-w-0 items-center gap-1 rounded-md border border-border/60 bg-card py-1.5 pr-1 pl-2.5\">\n      <code className=\"min-w-0 flex-1 truncate font-mono text-[11px]\">\n        {secret}\n      </code>\n      <CopyButton label=\"Copy API key\" value={secret} />\n    </div>\n    <div className=\"flex items-center justify-between gap-2\">\n      <p className=\"flex items-center gap-1.5 text-[10px] text-muted-foreground\">\n        <HugeiconsIcon\n          className=\"size-3 shrink-0\"\n          icon={Alert02Icon}\n          strokeWidth={2}\n        />\n        Copy it now. You will not see this key again.\n      </p>\n      <Button className=\"shrink-0\" onClick={onDone} size=\"xs\" variant=\"ghost\">\n        Done\n      </Button>\n    </div>\n  </div>\n);\n\nconst CreateForm = ({\n  pending,\n  onSubmit,\n  onCancel,\n}: {\n  pending: boolean;\n  onSubmit: (name: string, scope: ApiKeyScope) => void;\n  onCancel: () => void;\n}) => {\n  const [name, setName] = useState(\"\");\n  const [scope, setScope] = useState<ApiKeyScope>(\"personal\");\n\n  const submit = (event: FormEvent) => {\n    event.preventDefault();\n\n    if (name.trim()) {\n      onSubmit(name.trim(), scope);\n    }\n  };\n\n  return (\n    <form\n      className=\"fade-in-0 slide-in-from-top-1 flex animate-in flex-wrap items-center gap-2 duration-200 motion-reduce:animate-none\"\n      onSubmit={submit}\n    >\n      <Input\n        className=\"h-8 min-w-40 flex-1 rounded-md font-mono text-xs focus-visible:border-primary/50 focus-visible:ring-[3px] focus-visible:ring-primary/15\"\n        disabled={pending}\n        onChange={(event) => setName(event.target.value)}\n        placeholder=\"Key name\"\n        value={name}\n      />\n      <ScopeToggle onSelect={setScope} scope={scope} />\n      <Button\n        disabled={pending || name.trim().length === 0}\n        size=\"sm\"\n        type=\"submit\"\n      >\n        {pending ? \"Creating\\u2026\" : \"Create\"}\n      </Button>\n      <Button\n        disabled={pending}\n        onClick={onCancel}\n        size=\"sm\"\n        type=\"button\"\n        variant=\"ghost\"\n      >\n        Cancel\n      </Button>\n    </form>\n  );\n};\n\nconst HOLD_MS = 1000;\nconst RELEASE_MS = 180;\n\n/* Hold-to-confirm: a destructive fill sweeps left to right for HOLD_MS\n * (linear, it is progress), revealing a white copy of the label under the\n * same clip-path. Releasing early rewinds it. Pointer and keyboard both work.\n * Same grammar as the confirm-dialog. */\nconst HoldButton = ({\n  label,\n  onHold,\n}: {\n  label: string;\n  onHold: () => void;\n}) => {\n  const [holding, setHolding] = useState(false);\n  const timerRef = useRef(0);\n\n  const cancel = () => {\n    window.clearTimeout(timerRef.current);\n    setHolding(false);\n  };\n\n  const start = () => {\n    setHolding(true);\n    timerRef.current = window.setTimeout(() => {\n      setHolding(false);\n      onHold();\n    }, HOLD_MS);\n  };\n\n  useEffect(() => () => window.clearTimeout(timerRef.current), []);\n\n  return (\n    <Button\n      className=\"relative select-none overflow-hidden border border-destructive/50 bg-destructive/10 text-destructive hover:bg-destructive/15 hover:text-destructive\"\n      data-holding={holding || undefined}\n      onKeyDown={(event) => {\n        if (event.repeat || !(event.key === \"Enter\" || event.key === \" \")) {\n          return;\n        }\n\n        event.preventDefault();\n\n        if (!holding) {\n          start();\n        }\n      }}\n      onKeyUp={cancel}\n      onPointerCancel={cancel}\n      onPointerDown={start}\n      onPointerLeave={cancel}\n      onPointerUp={cancel}\n      size=\"xs\"\n      type=\"button\"\n      variant=\"ghost\"\n    >\n      <span className=\"relative\">{label}</span>\n      <span\n        aria-hidden=\"true\"\n        className=\"absolute inset-0 flex items-center justify-center bg-destructive text-destructive-foreground\"\n        style={{\n          clipPath: holding ? \"inset(0 0% 0 0)\" : \"inset(0 100% 0 0)\",\n          transition: `clip-path ${holding ? HOLD_MS : RELEASE_MS}ms ${\n            holding ? \"linear\" : \"ease-out\"\n          }`,\n        }}\n      >\n        {label}\n      </span>\n    </Button>\n  );\n};\n\nconst KeyRow = ({\n  apiKey,\n  revoking,\n  onAskRevoke,\n  onCancelRevoke,\n  onRevoke,\n}: {\n  apiKey: ApiKey;\n  revoking: boolean;\n  onAskRevoke: () => void;\n  onCancelRevoke: () => void;\n  onRevoke: () => void;\n}) => (\n  <li\n    className={cn(\n      \"group relative flex items-center gap-3 border-border/60 border-t py-3 transition-colors first:border-t-0 hover:bg-muted/20\",\n      // Hold the hover tint for the whole confirm so the mask matches.\n      revoking && \"bg-muted/20\"\n    )}\n    data-slot=\"api-key-row\"\n  >\n    <span className=\"flex size-8 shrink-0 items-center justify-center rounded-full border border-border/60 bg-background text-muted-foreground\">\n      <HugeiconsIcon\n        className=\"size-3.5\"\n        icon={SCOPE_ICON[apiKey.scope]}\n        strokeWidth={2}\n      />\n    </span>\n\n    <div className=\"min-w-0 flex-1\">\n      <p className=\"truncate font-mono font-medium text-foreground text-sm\">\n        {apiKey.name}\n      </p>\n      <p className=\"mt-0.5 text-[11px] text-muted-foreground/70\">\n        {SCOPE_SUBLABEL[apiKey.scope]}\n      </p>\n    </div>\n\n    <div className=\"shrink-0 text-right\">\n      <p className=\"font-mono text-[11px] text-muted-foreground tabular-nums\">\n        {`created ${apiKey.createdAt}`}\n      </p>\n      <p className=\"text-[10px] text-muted-foreground/60\">\n        {apiKey.lastUsedAt ? `used ${apiKey.lastUsedAt}` : \"never used\"}\n      </p>\n    </div>\n\n    <div className=\"flex shrink-0 justify-end\">\n      <Tooltip>\n        <TooltipTrigger\n          render={\n            <Button\n              aria-label={`Revoke ${apiKey.name}`}\n              className=\"text-muted-foreground/40 transition-colors hover:text-destructive group-hover:text-muted-foreground/70\"\n              onClick={onAskRevoke}\n              size=\"icon-xs\"\n              type=\"button\"\n              variant=\"ghost\"\n            >\n              <HugeiconsIcon icon={Delete02Icon} strokeWidth={2} />\n            </Button>\n          }\n        />\n        <TooltipContent>Revoke</TooltipContent>\n      </Tooltip>\n    </div>\n\n    {revoking ? (\n      <div className=\"fade-in-0 slide-in-from-right-2 absolute inset-y-0 right-0 flex animate-in items-center gap-1.5 bg-gradient-to-l from-[color-mix(in_srgb,var(--muted)_20%,var(--card))] via-[85%] via-[color-mix(in_srgb,var(--muted)_20%,var(--card))] to-transparent pl-16 duration-200 motion-reduce:animate-none\">\n        <Button onClick={onCancelRevoke} size=\"xs\" variant=\"ghost\">\n          Cancel\n        </Button>\n        <HoldButton label=\"Hold to revoke\" onHold={onRevoke} />\n      </div>\n    ) : null}\n  </li>\n);\n\nexport type ApiKeyListProps = Omit<\n  ComponentProps<\"div\">,\n  \"children\" | \"onChange\"\n> & {\n  /** The keys to list, in display order. */\n  keys: ApiKey[];\n  /**\n   * Create a key of the chosen scope; return the full token to reveal once.\n   * The create UI hides when omitted.\n   */\n  onCreate?: (name: string, scope: ApiKeyScope) => Promise<string> | string;\n  /** Revoke a key. Receives the whole key so the scope is known. */\n  onRevoke?: (key: ApiKey) => Promise<void> | void;\n  /** Card title. */\n  label?: string;\n  isLoading?: boolean;\n  error?: Error | string | null;\n};\n\nconst cardClassName =\n  \"flex w-full min-w-0 flex-col rounded-lg border border-border/60 bg-card p-4 shadow-none ring-0\";\n\nconst Header = ({\n  label,\n  actions,\n}: {\n  label: string;\n  actions?: ComponentProps<\"div\">[\"children\"];\n}) => (\n  <div className=\"flex items-center justify-between gap-3\">\n    <p className=\"font-medium text-foreground text-sm\">{label}</p>\n    {actions ? (\n      <div className=\"flex shrink-0 items-center gap-2\">{actions}</div>\n    ) : null}\n  </div>\n);\n\nconst emptyMessage = (total: number, filter: ScopeFilterValue) => {\n  if (total === 0) {\n    return \"No API keys yet.\";\n  }\n\n  return `No ${filter === \"organization\" ? \"organization\" : \"personal\"} keys.`;\n};\n\nconst HeaderActions = ({\n  showFilter,\n  filter,\n  onFilter,\n  showCreate,\n  onCreate,\n}: {\n  showFilter: boolean;\n  filter: ScopeFilterValue;\n  onFilter: (value: ScopeFilterValue) => void;\n  showCreate: boolean;\n  onCreate: () => void;\n}) => (\n  <>\n    {showFilter ? <ScopeFilter onChange={onFilter} value={filter} /> : null}\n    {showCreate ? (\n      <Button onClick={onCreate} size=\"sm\">\n        <HugeiconsIcon icon={PlusSignIcon} strokeWidth={2} />\n        New key\n      </Button>\n    ) : null}\n  </>\n);\n\n/**\n * Animate the body's height as it changes (create form opens, the reveal\n * panel appears, rows are added or revoked), so the card grows and shrinks\n * smoothly instead of jumping.\n */\nconst AnimatedHeight = ({ children }: { children: ReactNode }) => {\n  const innerRef = useRef<HTMLDivElement>(null);\n  const lastRef = useRef<number | null>(null);\n  const [height, setHeight] = useState<number>();\n  const [animating, setAnimating] = useState(false);\n\n  useEffect(() => {\n    const inner = innerRef.current;\n\n    if (!inner) {\n      return;\n    }\n\n    const observer = new ResizeObserver(() => {\n      const next = inner.offsetHeight;\n\n      if (lastRef.current !== null && lastRef.current !== next) {\n        setAnimating(true);\n      }\n\n      lastRef.current = next;\n      setHeight(next);\n    });\n\n    observer.observe(inner);\n    lastRef.current = inner.offsetHeight;\n    setHeight(inner.offsetHeight);\n\n    return () => observer.disconnect();\n  }, []);\n\n  return (\n    <div\n      className={cn(\n        \"transition-[height] duration-300 ease-out motion-reduce:transition-none\",\n        // Clip only while the height animates; let focus rings and shadows\n        // show once it settles.\n        animating ? \"overflow-hidden\" : \"overflow-visible\"\n      )}\n      onTransitionEnd={() => setAnimating(false)}\n      style={{ height }}\n    >\n      <div ref={innerRef}>{children}</div>\n    </div>\n  );\n};\n\nconst EmptyState = ({\n  message,\n  showHint,\n}: {\n  message: string;\n  showHint: boolean;\n}) => (\n  <div className=\"rounded-md border border-border/60 border-dashed bg-background px-3 py-6 text-center\">\n    <p className=\"text-muted-foreground text-xs\">{message}</p>\n    {showHint ? (\n      <p className=\"mt-1 text-[11px] text-muted-foreground/60\">\n        Create one to call the Neon API.\n      </p>\n    ) : null}\n  </div>\n);\n\nexport const ApiKeyList = ({\n  keys,\n  onCreate,\n  onRevoke,\n  label = \"API keys\",\n  isLoading = false,\n  error = null,\n  className,\n  ...props\n}: ApiKeyListProps) => {\n  const [creating, setCreating] = useState(false);\n  const [pending, setPending] = useState(false);\n  const [created, setCreated] = useState<{ name: string; secret: string }>();\n  const [revokingId, setRevokingId] = useState<string>();\n  const [filter, setFilter] = useState<ScopeFilterValue>(\"all\");\n\n  const handleCreate = async (name: string, scope: ApiKeyScope) => {\n    if (!onCreate) {\n      return;\n    }\n\n    setPending(true);\n\n    try {\n      const secret = await onCreate(name, scope);\n      setCreated({ name, secret });\n      setCreating(false);\n    } finally {\n      setPending(false);\n    }\n  };\n\n  const handleRevoke = async (key: ApiKey) => {\n    setRevokingId(undefined);\n    await onRevoke?.(key);\n  };\n\n  if (isLoading) {\n    return (\n      <div\n        aria-busy=\"true\"\n        className={cn(cardClassName, className)}\n        data-slot=\"api-key-list\"\n        {...props}\n      >\n        <Header label={label} />\n        <div className=\"mt-3 space-y-3\">\n          <Skeleton aria-hidden=\"true\" className=\"h-10 w-full\" />\n          <Skeleton aria-hidden=\"true\" className=\"h-10 w-full\" />\n        </div>\n      </div>\n    );\n  }\n\n  if (error) {\n    const message = typeof error === \"string\" ? error : error.message;\n\n    return (\n      <div\n        className={cn(cardClassName, className)}\n        data-slot=\"api-key-list\"\n        role=\"alert\"\n        {...props}\n      >\n        <Header label={label} />\n        <p className=\"mt-3 flex items-baseline gap-2 text-sm\">\n          <span className=\"font-mono text-destructive text-xs\">error</span>\n          <span className=\"text-foreground\">{message}</span>\n        </p>\n      </div>\n    );\n  }\n\n  const hasOrg = keys.some((key) => key.scope === \"organization\");\n  const hasPersonal = keys.some((key) => key.scope === \"personal\");\n  const showFilter = hasOrg && hasPersonal;\n  const filtered =\n    filter === \"all\" ? keys : keys.filter((key) => key.scope === filter);\n  const showCreateButton = Boolean(onCreate) && !(creating || created);\n  const showEmpty = filtered.length === 0 && !(creating || created);\n\n  return (\n    <div\n      className={cn(cardClassName, className)}\n      data-slot=\"api-key-list\"\n      {...props}\n    >\n      <TooltipProvider>\n        <Header\n          actions={\n            <HeaderActions\n              filter={filter}\n              onCreate={() => setCreating(true)}\n              onFilter={setFilter}\n              showCreate={showCreateButton}\n              showFilter={showFilter}\n            />\n          }\n          label={label}\n        />\n\n        <AnimatedHeight>\n          <div className=\"flex flex-col gap-3 pt-3\">\n            {creating ? (\n              <CreateForm\n                onCancel={() => setCreating(false)}\n                onSubmit={handleCreate}\n                pending={pending}\n              />\n            ) : null}\n\n            {created ? (\n              <RevealOncePanel\n                name={created.name}\n                onDone={() => setCreated(undefined)}\n                secret={created.secret}\n              />\n            ) : null}\n\n            {filtered.length > 0 ? (\n              <ul>\n                {filtered.map((apiKey) => (\n                  <KeyRow\n                    apiKey={apiKey}\n                    key={apiKey.id}\n                    onAskRevoke={() => setRevokingId(apiKey.id)}\n                    onCancelRevoke={() => setRevokingId(undefined)}\n                    onRevoke={() => handleRevoke(apiKey)}\n                    revoking={revokingId === apiKey.id}\n                  />\n                ))}\n              </ul>\n            ) : null}\n\n            {showEmpty ? (\n              <EmptyState\n                message={emptyMessage(keys.length, filter)}\n                showHint={Boolean(onCreate) && keys.length === 0}\n              />\n            ) : null}\n          </div>\n        </AnimatedHeight>\n      </TooltipProvider>\n    </div>\n  );\n};\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}