{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "secret-field",
  "title": "Secret Field",
  "description": "Secret Field from the Get IT Done design system.",
  "dependencies": [
    "lucide-react@^0.379.0"
  ],
  "registryDependencies": [
    "https://zero-design.itdone.de/r/button.json",
    "https://zero-design.itdone.de/r/input.json",
    "https://zero-design.itdone.de/r/labels-context.json",
    "https://zero-design.itdone.de/r/theme.json"
  ],
  "files": [
    {
      "path": "registry/ui/secret-field.tsx",
      "content": "'use client';\n\nimport { Check, Copy, Eye, EyeOff } from 'lucide-react';\nimport * as React from 'react';\n\nimport { Button } from '@/components/ui/button';\nimport { Input, type InputProps } from '@/components/ui/input';\nimport { useLabels } from '@/components/ui/labels-context';\n\n// Bullets shown between the visible ends. Fixed so the masked text does not reveal the secret length.\nconst MASK_BULLETS = 8;\nconst COPIED_RESET_MS = 2000;\n\nexport interface SecretFieldProps extends Omit<InputProps, 'value' | 'defaultValue' | 'type' | 'readOnly' | 'appearance' | 'revealable' | 'trailingAction' | 'variant' | 'onChange'> {\n  // The real value. It is only rendered into the field while revealed, and it is what the copy button copies.\n  value: string;\n  // Characters kept visible at the start and the end while masked. Short values are masked completely. Default 4.\n  visibleChars?: number;\n  // Controlled reveal state. Uncontrolled via `defaultRevealed`.\n  revealed?: boolean;\n  defaultRevealed?: boolean;\n  onRevealedChange?: (revealed: boolean) => void;\n  // Hides the reveal toggle or the copy button.\n  revealable?: boolean;\n  copyable?: boolean;\n  // Called with the real value after it was copied.\n  onValueCopied?: (value: string) => void;\n  // Accessible names of the buttons. Win over the provider text.\n  showLabel?: string;\n  hideLabel?: string;\n  copyLabel?: string;\n  copiedLabel?: string;\n}\n\n// Partially masks a secret, e.g. `sk_l••••••••3f9a`. Values too short to keep both ends are masked completely.\nfunction maskSecret(value: string, visibleChars: number) {\n  if (value.length <= visibleChars * 2 + MASK_BULLETS / 2) return '•'.repeat(Math.min(Math.max(value.length, 1), MASK_BULLETS));\n  return `${value.slice(0, visibleChars)}${'•'.repeat(MASK_BULLETS)}${value.slice(-visibleChars)}`;\n}\n\n// Read-only field for API keys and private URLs: partially masked, reveal toggle and a copy button that always copies the real value.\nconst SecretField = React.forwardRef<HTMLInputElement, SecretFieldProps>(\n  (\n    {\n      value,\n      visibleChars = 4,\n      revealed: revealedProp,\n      defaultRevealed = false,\n      onRevealedChange,\n      revealable = true,\n      copyable = true,\n      onValueCopied,\n      showLabel,\n      hideLabel,\n      copyLabel,\n      copiedLabel,\n      disabled,\n      inputClassName,\n      ...props\n    },\n    ref,\n  ) => {\n    const l = useLabels({ showSecret: showLabel, hideSecret: hideLabel, copyValue: copyLabel, copied: copiedLabel });\n    const [innerRevealed, setInnerRevealed] = React.useState(defaultRevealed);\n    const revealed = revealedProp ?? innerRevealed;\n    const [copied, setCopied] = React.useState(false);\n    const timer = React.useRef<ReturnType<typeof setTimeout>>();\n    React.useEffect(() => () => clearTimeout(timer.current), []);\n\n    const toggle = () => {\n      if (revealedProp === undefined) setInnerRevealed(!revealed);\n      onRevealedChange?.(!revealed);\n    };\n\n    const copy = async () => {\n      try {\n        await navigator.clipboard?.writeText(value);\n      } catch {\n        return;\n      }\n      onValueCopied?.(value);\n      setCopied(true);\n      clearTimeout(timer.current);\n      timer.current = setTimeout(() => setCopied(false), COPIED_RESET_MS);\n    };\n\n    const actions = (revealable ? 1 : 0) + (copyable ? 1 : 0);\n    const iconClass = 'h-icon-xs w-icon-xs';\n\n    return (\n      <>\n        <Input\n          ref={ref}\n          type=\"text\"\n          appearance=\"readonly\"\n          value={revealed ? value : maskSecret(value, visibleChars)}\n          disabled={disabled}\n          autoComplete=\"off\"\n          spellCheck={false}\n          data-secret-field=\"\"\n          data-revealed={revealed || undefined}\n          inputClassName={[actions === 2 ? 'pr-[calc(theme(spacing.control-sm)*2+theme(spacing.2))]' : undefined, inputClassName].filter(Boolean).join(' ') || undefined}\n          trailingAction={\n            actions > 0 ? (\n              <>\n                {revealable ? (\n                  <Button type=\"button\" variant=\"ghost\" size=\"control-sm\" className=\"w-control-sm px-0\" aria-label={revealed ? l.hideSecret : l.showSecret} disabled={disabled} onClick={toggle}>\n                    {revealed ? <EyeOff aria-hidden=\"true\" className={iconClass} /> : <Eye aria-hidden=\"true\" className={iconClass} />}\n                  </Button>\n                ) : null}\n                {copyable ? (\n                  <Button type=\"button\" variant=\"ghost\" size=\"control-sm\" className=\"w-control-sm px-0\" aria-label={copied ? l.copied : l.copyValue} disabled={disabled} onClick={copy}>\n                    {copied ? <Check aria-hidden=\"true\" className={iconClass} /> : <Copy aria-hidden=\"true\" className={iconClass} />}\n                  </Button>\n                ) : null}\n              </>\n            ) : undefined\n          }\n          {...props}\n        />\n        <span role=\"status\" className=\"sr-only\">\n          {copied ? l.copied : ''}\n        </span>\n      </>\n    );\n  },\n);\nSecretField.displayName = 'SecretField';\n\nexport { maskSecret, SecretField };\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}