{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "secret-field",
  "title": "Secret Field",
  "description": "An API key or token: hidden until asked for, copied whole either way.",
  "dependencies": [
    "lucide-react@^1.31.0"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/default/secret-field/secret-field.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Check, Clipboard, Eye, EyeOff } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport type SecretFieldProps = Omit<\n  React.HTMLAttributes<HTMLDivElement>,\n  \"children\" | \"onCopy\"\n> & {\n  value: string\n  /** Characters left readable at the start and end while it is hidden. */\n  visiblePrefix?: number\n  visibleSuffix?: number\n  masked?: boolean\n  defaultMasked?: boolean\n  onMaskedChange?: (masked: boolean) => void\n  /** Drop the reveal control for a value that should never be shown. */\n  revealable?: boolean\n  copyable?: boolean\n  onCopied?: (value: string) => void\n  label?: string\n}\n\nconst CONTROL =\n  \"text-muted-foreground hover:bg-muted hover:text-foreground inline-flex size-8 shrink-0 items-center justify-center rounded-md transition-colors duration-150 motion-reduce:transition-none\"\n\n/**\n * An API key, a token, a connection string. Hidden until asked for, copied\n * whole whether or not it is showing, and never announced as a run of dots.\n */\nexport function SecretField({\n  value,\n  visiblePrefix = 0,\n  visibleSuffix = 4,\n  masked: controlledMasked,\n  defaultMasked = true,\n  onMaskedChange,\n  revealable = true,\n  copyable = true,\n  onCopied,\n  label = \"Secret\",\n  className,\n  ...rootProps\n}: SecretFieldProps) {\n  const [ownMasked, setOwnMasked] = React.useState(defaultMasked)\n  const [copyState, setCopyState] = React.useState<\"idle\" | \"copied\" | \"error\">(\n    \"idle\"\n  )\n  const timer = React.useRef<ReturnType<typeof setTimeout> | null>(null)\n\n  React.useEffect(\n    () => () => {\n      if (timer.current) clearTimeout(timer.current)\n    },\n    []\n  )\n\n  const masked = (controlledMasked ?? ownMasked) && revealable !== false\n\n  const hidden = React.useMemo(() => {\n    const head = value.slice(0, visiblePrefix)\n    const tail = visibleSuffix > 0 ? value.slice(-visibleSuffix) : \"\"\n    const middle = Math.max(0, value.length - head.length - tail.length)\n\n    return `${head}${\"•\".repeat(Math.min(middle, 24))}${tail}`\n  }, [value, visiblePrefix, visibleSuffix])\n\n  function toggle() {\n    const next = !masked\n    if (controlledMasked === undefined) setOwnMasked(next)\n    onMaskedChange?.(next)\n  }\n\n  async function copy() {\n    try {\n      await navigator.clipboard.writeText(value)\n      setCopyState(\"copied\")\n      onCopied?.(value)\n    } catch {\n      // Denied permission, an insecure context, or a sandboxed frame.\n      setCopyState(\"error\")\n    }\n\n    if (timer.current) clearTimeout(timer.current)\n    timer.current = setTimeout(() => setCopyState(\"idle\"), 1600)\n  }\n\n  return (\n    <div\n      data-slot=\"secret-field\"\n      data-masked={masked || undefined}\n      className={cn(\n        \"border-border bg-muted/40 flex min-w-0 items-center gap-2 rounded-[calc(var(--radius)+0.15rem)] border px-3 py-1.5\",\n        className\n      )}\n      {...rootProps}\n    >\n      {/*\n       * A masked value is read as a row of bullets, which is worse than\n       * useless, so the text is hidden and the state is said in words.\n       */}\n      <code\n        aria-hidden={masked || undefined}\n        data-slot=\"secret-field-value\"\n        className=\"min-w-0 flex-1 truncate font-[family-name:var(--font-mono),monospace] text-xs\"\n      >\n        {masked ? hidden : value}\n      </code>\n      {masked ? (\n        <span className=\"sr-only\">{label} hidden</span>\n      ) : (\n        <span className=\"sr-only\">{label} showing</span>\n      )}\n\n      {revealable && (\n        <button\n          type=\"button\"\n          data-slot=\"secret-field-reveal\"\n          aria-label={\n            masked\n              ? `Show ${label.toLowerCase()}`\n              : `Hide ${label.toLowerCase()}`\n          }\n          aria-pressed={!masked}\n          className={CONTROL}\n          onClick={toggle}\n        >\n          {masked ? (\n            <Eye aria-hidden=\"true\" size={14} />\n          ) : (\n            <EyeOff aria-hidden=\"true\" size={14} />\n          )}\n        </button>\n      )}\n\n      {copyable && (\n        <button\n          type=\"button\"\n          data-slot=\"secret-field-copy\"\n          aria-label={\n            copyState === \"copied\"\n              ? \"Copied\"\n              : copyState === \"error\"\n                ? \"Copy failed\"\n                : `Copy ${label.toLowerCase()}`\n          }\n          className={CONTROL}\n          onClick={copy}\n        >\n          {copyState === \"copied\" ? (\n            <Check aria-hidden=\"true\" size={14} />\n          ) : (\n            <Clipboard aria-hidden=\"true\" size={14} />\n          )}\n        </button>\n      )}\n\n      <span aria-live=\"polite\" className=\"sr-only\">\n        {copyState === \"copied\"\n          ? `${label} copied to clipboard.`\n          : copyState === \"error\"\n            ? `${label} could not be copied.`\n            : \"\"}\n      </span>\n    </div>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "security",
    "clipboard",
    "control"
  ],
  "type": "registry:ui"
}