{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "input",
  "type": "registry:ui",
  "title": "Input",
  "description": "Labeled text field that wires a real label to the input for you, with an optional hint, error and password reveal.",
  "categories": [
    "forms"
  ],
  "registryDependencies": [
    "https://whiskeyjack.net/r/utils.json"
  ],
  "files": [
    {
      "path": "components/ui/input.tsx",
      "type": "registry:ui",
      "target": "components/ui/input.tsx",
      "content": "import * as React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface InputProps\n  extends React.InputHTMLAttributes<HTMLInputElement> {\n  /**\n   * Optional label rendered above the input as a real `<label>` element.\n   * When provided, an `id` is auto-generated (via `useId`) and associated\n   * with the input via `htmlFor`. Any `id` or `aria-label` passed explicitly\n   * in `props` takes precedence.\n   *\n   * Matches the convention used across the apps:\n   * `block text-sm font-medium` in the secondary text color above the field.\n   */\n  label?: React.ReactNode;\n  /**\n   * Supporting text below the field -- a format, a range, a minimum. Reaches\n   * the field through `aria-describedby`, so it is read with the label rather\n   * than as a separate stop.\n   */\n  hint?: React.ReactNode;\n  /**\n   * What is wrong with the current value. Replaces `hint` while set, marks the\n   * field `aria-invalid`, and announces through `role=\"alert\"` -- a field whose\n   * submit button greys out with nothing on screen leaves the user guessing\n   * which of the form's values it objected to.\n   */\n  error?: React.ReactNode;\n  /**\n   * Accessible name for the reveal button on a `type=\"password\"` field, in the\n   * masked state (\"Show password\").\n   *\n   * The button renders only when BOTH this and `hideLabel` are given: the DS\n   * ships no translations, and an icon-only control with no name is worse than\n   * no control at all.\n   */\n  revealLabel?: string;\n  /** Accessible name for the same button while the value is visible (\"Hide password\"). */\n  hideLabel?: string;\n}\n\n/** Open eye / struck-through eye, at the size the field's trailing slot allows. */\nfunction EyeIcon({ off }: { off: boolean }) {\n  return (\n    <svg\n      className=\"w-4 h-4\"\n      viewBox=\"0 0 20 20\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth=\"1.6\"\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      aria-hidden\n    >\n      <path d=\"M1.5 10S4.7 4.5 10 4.5 18.5 10 18.5 10 15.3 15.5 10 15.5 1.5 10 1.5 10Z\" />\n      <circle cx=\"10\" cy=\"10\" r=\"2.5\" />\n      {off && <path d=\"M3 17 17 3\" />}\n    </svg>\n  );\n}\n\nconst Input = React.forwardRef<HTMLInputElement, InputProps>(\n  (\n    {\n      className,\n      type,\n      label,\n      hint,\n      error,\n      revealLabel,\n      hideLabel,\n      id: idProp,\n      \"aria-describedby\": describedByProp,\n      ...props\n    },\n    ref\n  ) => {\n    const autoId = React.useId();\n    const id = idProp ?? (label !== undefined ? autoId : undefined);\n    const messageId = `${autoId}-message`;\n\n    const [revealed, setRevealed] = React.useState(false);\n    const canReveal =\n      type === \"password\" && revealLabel !== undefined && hideLabel !== undefined;\n    // Swapping the type is what reveals the value; the field keeps its own\n    // state so a parent re-render does not re-mask a value mid-read.\n    const resolvedType = canReveal && revealed ? \"text\" : type;\n\n    const message = error ?? hint;\n    const describedBy =\n      [describedByProp, message !== undefined ? messageId : undefined]\n        .filter(Boolean)\n        .join(\" \") || undefined;\n\n    const inputEl = (\n      <input\n        type={resolvedType}\n        id={id}\n        aria-invalid={error !== undefined ? true : props[\"aria-invalid\"]}\n        aria-describedby={describedBy}\n        className={cn(\n          // Field shape: regular radius (a button is the full-round one), a\n          // solid border, no fill of its own, and the deboss recess -- so a\n          // field reads as cut INTO whatever it sits on while a button sits on\n          // top of it. Transparent rather than `surface`, so it takes the tone\n          // of its container instead of punching a white well into a tinted one.\n          \"flex h-control-md w-full rounded-lg\",\n          \"border border-[var(--color-border-light)]\",\n          \"bg-transparent shadow-deboss\",\n          \"px-3 py-2 text-sm\",\n          \"file:border-0 file:bg-transparent file:text-sm file:font-medium\",\n          \"placeholder:text-[var(--color-text-muted-light)]\",\n          \"wj-focus-ring\",\n          \"disabled:cursor-not-allowed disabled:opacity-50\",\n          \"hover:border-[var(--color-warm-300)]\",\n          \"disabled:hover:border-[var(--color-border-light)]\",\n          \"transition-colors\",\n          \"dark:border-[var(--color-border-dark)]\",\n          \"dark:hover:border-[var(--color-neutral-700)]\",\n          \"dark:disabled:hover:border-[var(--color-border-dark)]\",\n          \"dark:placeholder:text-[var(--color-text-muted-dark)]\",\n          // Clear the trailing reveal button, and take the error border over\n          // the resting one in both themes.\n          canReveal && \"pe-11\",\n          error !== undefined &&\n            \"border-[var(--color-error-500)] dark:border-[var(--color-error-500)] hover:border-[var(--color-error-500)] dark:hover:border-[var(--color-error-500)]\",\n          className\n        )}\n        ref={ref}\n        {...props}\n      />\n    );\n\n    // The field alone, with nothing to wrap it for: the common case stays a\n    // bare <input> so a caller's layout is unchanged.\n    if (label === undefined && message === undefined && !canReveal) {\n      return inputEl;\n    }\n\n    const field = canReveal ? (\n      <div className=\"relative\">\n        {inputEl}\n        <button\n          type=\"button\"\n          // Takes no tab stop of its own ahead of the field it belongs to: it\n          // sits after the input in the DOM, so Tab reaches it in reading order.\n          onClick={() => setRevealed((v) => !v)}\n          // The name flips between the two labels, which already states the\n          // state; an aria-pressed on top would read as \"Hide password, pressed\".\n          aria-label={revealed ? hideLabel : revealLabel}\n          title={revealed ? hideLabel : revealLabel}\n          className={cn(\n            \"wj-focus-ring absolute end-1 top-1/2 -translate-y-1/2\",\n            \"flex h-8 w-8 items-center justify-center rounded-full\",\n            \"text-[var(--color-text-muted-light)] dark:text-[var(--color-text-muted-dark)]\",\n            \"hover:text-[var(--color-text-primary-light)] dark:hover:text-[var(--color-text-primary-dark)]\"\n          )}\n        >\n          <EyeIcon off={revealed} />\n        </button>\n      </div>\n    ) : (\n      inputEl\n    );\n\n    return (\n      <div className=\"flex flex-col gap-1\">\n        {label !== undefined && (\n          <label\n            htmlFor={id}\n            className=\"block text-sm font-medium text-[var(--color-text-secondary-light)] dark:text-[var(--color-text-secondary-dark)]\"\n          >\n            {label}\n          </label>\n        )}\n        {field}\n        {message !== undefined && (\n          <p\n            id={messageId}\n            // Only the error is a live region -- a hint is static supporting\n            // text, and announcing it on render would talk over the label.\n            role={error !== undefined ? \"alert\" : undefined}\n            className={cn(\n              \"text-xs\",\n              error !== undefined\n                ? \"text-[var(--color-error-700)] dark:text-[var(--color-error-500)]\"\n                : \"text-[var(--color-text-muted-light)] dark:text-[var(--color-text-muted-dark)]\"\n            )}\n          >\n            {message}\n          </p>\n        )}\n      </div>\n    );\n  }\n);\nInput.displayName = \"Input\";\n\nexport { Input };\n"
    }
  ],
  "docs": "Pass `label` and it renders a real label element tied to the input; otherwise supply your own aria-label. A placeholder is a hint, so keep the label for the field's name. Native input attributes are forwarded, so type, placeholder, and validation work as usual. `hint` adds supporting text below the field and `error` replaces it, marking the field aria-invalid and announcing through role=alert -- reach for `error` whenever a submit button greys out, so the reason is on screen rather than guessed at. On a `type=\"password\"` field, passing BOTH `revealLabel` and `hideLabel` adds a show/hide toggle; it renders only when both are given, since an icon-only control with no accessible name is worse than no control.",
  "meta": {
    "group": "forms",
    "related": [
      "select",
      "slider"
    ],
    "exports": [
      "Input"
    ],
    "siteSlug": "input"
  }
}
