{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "pin-input",
  "title": "PIN Input",
  "description": "Masked PIN/passphrase input with paste support, show/hide toggle, and accessible loading/error states. No verification logic of its own.",
  "registryDependencies": [
    "https://knock.codes/r/react/knock-codes-types.json"
  ],
  "files": [
    {
      "path": "packages/react/PinInput.tsx",
      "content": "\"use client\";\r\n\r\nimport { useId, useState, type FormEvent } from \"react\";\r\nimport { DEFAULT_LABELS, type KnockCodesError, type KnockCodesLabels } from \"./types.ts\";\r\nimport { cx } from \"./cx.ts\";\r\n\r\n/**\r\n * Presentational PIN/OTP input — no verification logic of its own. It's a\r\n * single masked text field (native paste support comes for free), not a\r\n * segmented per-character box grid — the product encourages arbitrary-length\r\n * passphrases, which a fixed-length box UI can't host.\r\n */\r\nexport interface PinInputProps {\r\n  value: string;\r\n  onChange: (value: string) => void;\r\n  onSubmit: () => void;\r\n  submitting: boolean;\r\n  error: KnockCodesError | null;\r\n  labels?: KnockCodesLabels;\r\n  autoFocus?: boolean;\r\n  /** Shown under the field while there's no error yet. */\r\n  helperText?: string;\r\n  /** @default \"access-code\" */\r\n  name?: string;\r\n  /** @default \"off\" */\r\n  autoComplete?: string;\r\n}\r\n\r\nfunction EyeIcon() {\r\n  return (\r\n    <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth={1.75} className=\"h-4 w-4\" aria-hidden=\"true\">\r\n      <path d=\"M1.5 12S5 5 12 5s10.5 7 10.5 7-3.5 7-10.5 7-10.5-7-10.5-7Z\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\r\n      <circle cx=\"12\" cy=\"12\" r=\"3\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\r\n    </svg>\r\n  );\r\n}\r\n\r\nfunction EyeOffIcon() {\r\n  return (\r\n    <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth={1.75} className=\"h-4 w-4\" aria-hidden=\"true\">\r\n      <path\r\n        d=\"M3 3l18 18M10.6 10.6a3 3 0 0 0 4.24 4.24M6.7 6.7C4.5 8.1 3 10.2 1.5 12c0 0 3.5 7 10.5 7 2.1 0 3.87-.62 5.31-1.53M9.9 4.24A11.6 11.6 0 0 1 12 4c7 0 10.5 8 10.5 8a17.9 17.9 0 0 1-2.16 3.19\"\r\n        strokeLinecap=\"round\"\r\n        strokeLinejoin=\"round\"\r\n      />\r\n    </svg>\r\n  );\r\n}\r\n\r\nexport function PinInput({\r\n  value,\r\n  onChange,\r\n  onSubmit,\r\n  submitting,\r\n  error,\r\n  labels,\r\n  autoFocus,\r\n  helperText,\r\n  name = \"access-code\",\r\n  autoComplete = \"off\",\r\n}: PinInputProps) {\r\n  const merged = { ...DEFAULT_LABELS, ...labels };\r\n  const [revealed, setRevealed] = useState(false);\r\n  const inputId = useId();\r\n  const statusId = useId();\r\n\r\n  const handleSubmit = (event: FormEvent) => {\r\n    event.preventDefault();\r\n    if (submitting || value.length === 0) return;\r\n    onSubmit();\r\n  };\r\n\r\n  const errorMessage = error\r\n    ? error.reason === \"network\"\r\n      ? merged.networkErrorMessage\r\n      : merged.invalidErrorMessage\r\n    : null;\r\n\r\n  return (\r\n    <form onSubmit={handleSubmit} noValidate className=\"space-y-3\">\r\n      <div>\r\n        <label htmlFor={inputId} className=\"mb-1.5 block text-xs font-medium text-gray-500 dark:text-gray-400\">\r\n          {merged.inputLabel}\r\n        </label>\r\n        <div className=\"relative\">\r\n          <input\r\n            id={inputId}\r\n            name={name}\r\n            type={revealed ? \"text\" : \"password\"}\r\n            inputMode=\"text\"\r\n            value={value}\r\n            onChange={(event) => onChange(event.target.value)}\r\n            placeholder={merged.placeholder}\r\n            disabled={submitting}\r\n            autoFocus={autoFocus}\r\n            autoComplete={autoComplete}\r\n            aria-describedby={statusId}\r\n            aria-invalid={error ? true : undefined}\r\n            className={cx(\r\n              \"h-10 w-full rounded-[var(--ag-radius,0.5rem)] border px-3 pr-16 text-sm text-gray-900 focus:outline-none focus:ring-2 disabled:opacity-60 dark:bg-[var(--ag-card-dark,#111827)] dark:text-gray-50\",\r\n              error\r\n                ? \"border-red-500 focus:border-red-500 focus:ring-red-500/25 dark:border-red-400\"\r\n                : \"border-[var(--ag-border,#d1d5db)] focus:border-[var(--ag-primary,#3b82f6)] focus:ring-[var(--ag-primary,#3b82f6)]/30 dark:border-[var(--ag-border-dark,#374151)]\"\r\n            )}\r\n          />\r\n          <button\r\n            type=\"button\"\r\n            onClick={() => setRevealed((current) => !current)}\r\n            aria-label={revealed ? merged.hideCodeLabel : merged.showCodeLabel}\r\n            title={revealed ? merged.hideCodeLabel : merged.showCodeLabel}\r\n            className=\"absolute top-1/2 right-3 -translate-y-1/2 text-xs font-medium text-gray-400 hover:text-gray-600 dark:hover:text-gray-300\"\r\n          >\r\n            {revealed ? merged.hideCodeLabel : merged.showCodeLabel}\r\n          </button>\r\n        </div>\r\n        <div\r\n          role=\"status\"\r\n          aria-live=\"polite\"\r\n          id={statusId}\r\n          className={cx(\r\n            \"mt-2 min-h-[1.1rem] text-xs\",\r\n            errorMessage ? \"font-medium text-red-600 dark:text-red-400\" : \"text-gray-500 dark:text-gray-400\"\r\n          )}\r\n        >\r\n          {submitting ? merged.submittingLabel : (errorMessage ?? helperText ?? \"\")}\r\n        </div>\r\n      </div>\r\n      <button\r\n        type=\"submit\"\r\n        disabled={submitting || value.length === 0}\r\n        className=\"mt-3 w-full rounded-[var(--ag-radius,0.5rem)] bg-[var(--ag-primary,#111827)] px-4 py-2.5 text-sm font-semibold text-[var(--ag-primary-fg,#ffffff)] transition-colors hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-[var(--ag-primary-dark,#f9fafb)] dark:text-[var(--ag-primary-fg-dark,#111827)]\"\r\n      >\r\n        {submitting ? merged.submittingLabel : merged.submitLabel}\r\n      </button>\r\n    </form>\r\n  );\r\n}\r\n",
      "type": "registry:file",
      "target": "components/knock-codes/react/PinInput.tsx"
    }
  ],
  "type": "registry:block"
}