{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "api-key-field",
  "title": "API Key Field",
  "description": "A settings-page secret field with fixed-length masking, reveal/hide with optional auto re-mask, copy-to-clipboard with a visible failure fallback, and a two-step confirm before regenerating.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/registry/ui/api-key-field.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Check, Copy, Eye, EyeOff, RefreshCw, TriangleAlert } from \"lucide-react\"\nimport { cn } from \"@/lib/utils\"\n\n/** The masked middle is always this many dots — fixed length, so the real key length never leaks. */\nconst MASK_DOT_COUNT = 12\n/** How long the \"Copied\" / \"Copy failed\" line stays visible before resetting to idle. */\nconst COPY_STATUS_DELAY = 2000\n\nexport interface ApiKeyFieldProps extends Omit<React.HTMLAttributes<HTMLDivElement>, \"onCopy\" | \"children\"> {\n  /** The real secret. Rendered verbatim once revealed — this component never reformats it. */\n  value: string\n  label?: string\n  /** Start already revealed instead of masked. */\n  defaultRevealed?: boolean\n  /** ms after a reveal before it auto-masks again. 0 (default) = never auto-hide. */\n  revealTimeout?: number\n  /** Prefix characters kept visible in the mask. */\n  keepPrefix?: number\n  /** Suffix characters kept visible in the mask. */\n  keepSuffix?: number\n  /** Fires once after a successful clipboard write. */\n  onCopy?: () => void\n  /** Called only after the two-step confirm; return a Promise to drive the built-in pending state. */\n  onRegenerate?: () => void | Promise<void>\n  /** External pending flag, OR'd with the promise-driven internal one — set it if the caller tracks its own request lifecycle instead of relying on onRegenerate's return value. */\n  regenerating?: boolean\n  helper?: React.ReactNode\n  /** Already-formatted \"created\" string — this component never formats dates itself. */\n  createdAtLabel?: string\n  disabled?: boolean\n}\n\n/** `sk_live_••••••••••••3f9a` — fixed-length dots in the middle, real prefix/suffix at the ends. */\nfunction maskValue(value: string, keepPrefix: number, keepSuffix: number) {\n  const dots = \"•\".repeat(MASK_DOT_COUNT)\n  const prefixLen = Math.max(0, keepPrefix)\n  const suffixLen = Math.max(0, keepSuffix)\n  if (value.length <= prefixLen + suffixLen) return dots\n  const prefix = value.slice(0, prefixLen)\n  const suffix = suffixLen > 0 ? value.slice(value.length - suffixLen) : \"\"\n  return `${prefix}${dots}${suffix}`\n}\n\nexport const ApiKeyField = React.forwardRef<HTMLDivElement, ApiKeyFieldProps>(\n  (\n    {\n      value,\n      label,\n      defaultRevealed = false,\n      revealTimeout = 0,\n      keepPrefix = 7,\n      keepSuffix = 4,\n      onCopy,\n      onRegenerate,\n      regenerating = false,\n      helper,\n      createdAtLabel,\n      disabled = false,\n      className,\n      ...props\n    },\n    ref,\n  ) => {\n    const inputId = React.useId()\n    const inputRef = React.useRef<HTMLInputElement>(null)\n    const revealTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)\n    const copyTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null)\n    const mountedRef = React.useRef(true)\n\n    const [revealed, setRevealed] = React.useState(defaultRevealed)\n    const [revealAnnouncement, setRevealAnnouncement] = React.useState(\"\")\n    const [copyStatus, setCopyStatus] = React.useState<\"idle\" | \"copied\" | \"failed\">(\"idle\")\n    const [regenerateStep, setRegenerateStep] = React.useState<\"idle\" | \"confirming\">(\"idle\")\n    const [selfPending, setSelfPending] = React.useState(false)\n\n    const pending = regenerating || selfPending\n\n    const clearRevealTimer = () => {\n      if (revealTimerRef.current !== null) {\n        clearTimeout(revealTimerRef.current)\n        revealTimerRef.current = null\n      }\n    }\n\n    const clearCopyTimer = () => {\n      if (copyTimerRef.current !== null) {\n        clearTimeout(copyTimerRef.current)\n        copyTimerRef.current = null\n      }\n    }\n\n    // Unmount: release both timers and stop honoring any in-flight regenerate promise.\n    React.useEffect(() => {\n      // Re-armed on mount: StrictMode runs mount → cleanup → mount in dev, so a\n      // flag that is only ever set to false would stay false for the live\n      // instance and the pending spinner would never clear.\n      mountedRef.current = true\n      return () => {\n        mountedRef.current = false\n        clearRevealTimer()\n        clearCopyTimer()\n      }\n    }, [])\n\n    /** Reveal + (re)arm the auto-hide timer. Shared by the eye button and the manual-copy fallback. */\n    const revealTemporarily = () => {\n      setRevealed(true)\n      setRevealAnnouncement(\"API key revealed\")\n      clearRevealTimer()\n      if (revealTimeout > 0) {\n        revealTimerRef.current = setTimeout(() => {\n          setRevealed(false)\n          setRevealAnnouncement(\"API key hidden\")\n          revealTimerRef.current = null\n        }, revealTimeout)\n      }\n    }\n\n    const toggleReveal = () => {\n      if (disabled) return\n      if (revealed) {\n        clearRevealTimer()\n        setRevealed(false)\n        setRevealAnnouncement(\"API key hidden\")\n        return\n      }\n      revealTemporarily()\n    }\n\n    const announceCopy = (status: \"copied\" | \"failed\") => {\n      clearCopyTimer()\n      setCopyStatus(status)\n      copyTimerRef.current = setTimeout(() => {\n        setCopyStatus(\"idle\")\n        copyTimerRef.current = null\n      }, COPY_STATUS_DELAY)\n    }\n\n    // Manual-copy fallback. Selecting a *masked* field would tell the user to\n    // copy a row of dots, so reveal the real value first (auto-hide timer armed\n    // as usual), then select it. Selection has to wait a frame: the input is\n    // still rendering the mask when this runs.\n    const selectForManualCopy = () => {\n      if (!revealed) revealTemporarily()\n      requestAnimationFrame(() => inputRef.current?.select())\n    }\n\n    const handleCopy = () => {\n      if (disabled) return\n      if (typeof navigator === \"undefined\" || !navigator.clipboard?.writeText) {\n        // No Clipboard API (insecure context / unsupported browser) — reveal and\n        // select so the user can still Ctrl/Cmd+C manually, and say so instead of\n        // failing silently.\n        selectForManualCopy()\n        announceCopy(\"failed\")\n        return\n      }\n      navigator.clipboard.writeText(value).then(\n        () => {\n          onCopy?.()\n          announceCopy(\"copied\")\n        },\n        () => {\n          selectForManualCopy()\n          announceCopy(\"failed\")\n        },\n      )\n    }\n\n    const handleRegenerateClick = () => {\n      if (disabled || pending) return\n      setRegenerateStep(\"confirming\")\n    }\n\n    const cancelRegenerate = () => setRegenerateStep(\"idle\")\n\n    const confirmRegenerate = () => {\n      setRegenerateStep(\"idle\")\n      const result = onRegenerate?.()\n      if (result instanceof Promise) {\n        setSelfPending(true)\n        result.finally(() => {\n          if (mountedRef.current) setSelfPending(false)\n        })\n      }\n    }\n\n    const displayValue = revealed ? value : maskValue(value, keepPrefix, keepSuffix)\n\n    return (\n      <div className={cn(\"flex w-full min-w-0 flex-col gap-1.5\", className)} ref={ref} {...props}>\n        {label && (\n          <label className=\"text-sm font-medium\" htmlFor={inputId}>\n            {label}\n          </label>\n        )}\n\n        <div\n          className={cn(\n            \"flex h-9 w-full min-w-0 items-center gap-1 rounded-md border border-input bg-transparent px-3 shadow-xs transition-colors\",\n            \"focus-within:border-ring focus-within:ring-3 focus-within:ring-ring/50\",\n            disabled && \"cursor-not-allowed opacity-50\",\n          )}\n        >\n          <input\n            aria-label={label ? `${label} value` : \"API key value\"}\n            className=\"min-w-0 flex-1 bg-transparent font-mono text-sm outline-none disabled:cursor-not-allowed\"\n            disabled={disabled}\n            id={inputId}\n            readOnly\n            ref={inputRef}\n            value={displayValue}\n          />\n\n          <button\n            aria-label={revealed ? \"Hide API key\" : \"Reveal API key\"}\n            aria-pressed={revealed}\n            className=\"inline-flex size-7 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-40\"\n            disabled={disabled}\n            onClick={toggleReveal}\n            type=\"button\"\n          >\n            {revealed ? (\n              <EyeOff aria-hidden=\"true\" className=\"size-4\" />\n            ) : (\n              <Eye aria-hidden=\"true\" className=\"size-4\" />\n            )}\n          </button>\n\n          <button\n            aria-label=\"Copy API key\"\n            className=\"inline-flex size-7 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-40\"\n            disabled={disabled}\n            onClick={handleCopy}\n            type=\"button\"\n          >\n            {copyStatus === \"copied\" ? (\n              <Check aria-hidden=\"true\" className=\"size-4\" style={{ color: \"var(--chart-2)\" }} />\n            ) : copyStatus === \"failed\" ? (\n              <TriangleAlert aria-hidden=\"true\" className=\"size-4 text-destructive\" />\n            ) : (\n              <Copy aria-hidden=\"true\" className=\"size-4\" />\n            )}\n          </button>\n        </div>\n\n        {/* Persistent live regions (never unmounted) so screen readers pick up every\n            transition; the copy one doubles as a visible on-page confirmation. */}\n        <div aria-live=\"polite\" className=\"min-h-4 text-xs\">\n          {copyStatus === \"copied\" && <span className=\"text-muted-foreground\">Copied</span>}\n          {copyStatus === \"failed\" && (\n            <span className=\"text-destructive\">Copy failed — key selected, press Ctrl/Cmd+C</span>\n          )}\n        </div>\n        <span aria-live=\"polite\" className=\"sr-only\">\n          {revealAnnouncement}\n        </span>\n\n        {(helper || createdAtLabel) && (\n          <div className=\"flex flex-wrap items-center justify-between gap-x-3 gap-y-1 text-xs text-muted-foreground\">\n            {helper && <span>{helper}</span>}\n            {createdAtLabel && <span className=\"shrink-0\">Created {createdAtLabel}</span>}\n          </div>\n        )}\n\n        {onRegenerate && (\n          <div className=\"flex flex-wrap items-center gap-2 text-sm\">\n            {pending ? (\n              <span className=\"inline-flex items-center gap-1.5 text-muted-foreground\">\n                <RefreshCw aria-hidden=\"true\" className=\"size-3.5 animate-spin motion-reduce:animate-none\" />\n                Regenerating…\n              </span>\n            ) : regenerateStep === \"confirming\" ? (\n              <>\n                <span className=\"text-muted-foreground\">Confirm regenerate?</span>\n                <button\n                  className=\"font-medium text-destructive underline-offset-2 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n                  onClick={confirmRegenerate}\n                  type=\"button\"\n                >\n                  Confirm\n                </button>\n                <button\n                  className=\"text-muted-foreground underline-offset-2 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\"\n                  onClick={cancelRegenerate}\n                  type=\"button\"\n                >\n                  Cancel\n                </button>\n              </>\n            ) : (\n              <button\n                className=\"inline-flex items-center gap-1.5 text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-40\"\n                disabled={disabled}\n                onClick={handleRegenerateClick}\n                type=\"button\"\n              >\n                <RefreshCw aria-hidden=\"true\" className=\"size-3.5\" />\n                Regenerate\n              </button>\n            )}\n          </div>\n        )}\n      </div>\n    )\n  },\n)\n\nApiKeyField.displayName = \"ApiKeyField\"\n\nexport default ApiKeyField\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}