{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"input","type":"registry:component","title":"Input","description":"A one-line field with a label, hint and error that trade places, a clear button that folds in, and one shake when an error arrives.","author":"Hairline UI","dependencies":[],"registryDependencies":["https://hairlineui.com/r/ui-interaction-motion.json?as=dependency"],"files":[{"path":"registry/input.tsx","type":"registry:component","content":"// Hairline UI · input · MIT License\n// Copyright (c) 2026 Hairline UI · https://hairlineui.com/license#components\n\n\"use client\";\n\nimport * as React from \"react\";\nimport { Field, fieldSurface, setNativeValue, useFieldIds, useShake, type FieldSize } from \"./field\";\nimport { EASE_OUT, INTERACTION_FOCUS, interactionIcon, useInteractionReducedMotion } from \"./ui-interaction-motion\";\n\n/* ─────────────────────────────────────────────────────────\n * INPUT: a one-line field\n *\n *   rest     a hairline pill; hover darkens the line\n *   focus    the line turns to the ring with a 3px halo\n *   typing   a clear button folds in once there is text and\n *            folds away when it's gone (it hands focus back)\n *   error    the line turns red, the hint gives way to the\n *            error and the field shakes once\n * ───────────────────────────────────────────────────────── */\n\nexport interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, \"size\"> {\n  label?: React.ReactNode;\n  hint?: React.ReactNode;\n  /** Shows red and replaces the hint. */\n  error?: React.ReactNode;\n  /** An icon or text before the value. */\n  leading?: React.ReactNode;\n  /** An icon, text or button after the value. */\n  trailing?: React.ReactNode;\n  /** A button that empties the field once it has text. */\n  clearable?: boolean;\n  /** 28px or 36px tall. */\n  size?: FieldSize;\n  /** Called with the new text on every change. */\n  onValueChange?: (value: string) => void;\n  ref?: React.Ref<HTMLInputElement>;\n  /** Class for the outer column. */\n  className?: string;\n  /** Class for the input element itself. */\n  inputClassName?: string;\n}\n\nfunction ClearIcon() {\n  return (\n    <svg viewBox=\"0 0 16 16\" fill=\"none\" className=\"size-3.5\">\n      <path d=\"M4.5 4.5l7 7M11.5 4.5l-7 7\" stroke=\"currentColor\" strokeWidth=\"1.6\" strokeLinecap=\"round\" />\n    </svg>\n  );\n}\n\nexport function Input({\n  label,\n  hint,\n  error,\n  leading,\n  trailing,\n  clearable,\n  size = \"md\",\n  onValueChange,\n  ref,\n  id,\n  className = \"\",\n  inputClassName = \"\",\n  value,\n  defaultValue,\n  onChange,\n  disabled,\n  required,\n  ...props\n}: InputProps) {\n  const reduced = useInteractionReducedMotion();\n  const ids = useFieldIds(id);\n  const inner = React.useRef<HTMLInputElement>(null);\n  const [text, setText] = React.useState(String(defaultValue ?? \"\"));\n  const current = value !== undefined ? String(value) : text;\n  const shakeRef = useShake<HTMLDivElement>(!!error);\n  const sm = size === \"sm\";\n  const showClear = !!clearable && current.length > 0 && !disabled;\n\n  const setRefs = (el: HTMLInputElement | null) => {\n    inner.current = el;\n    if (typeof ref === \"function\") ref(el);\n    else if (ref) (ref as React.RefObject<HTMLInputElement | null>).current = el;\n  };\n\n  const describedBy = [hint && !error ? ids.hint : null, error ? ids.error : null].filter(Boolean).join(\" \") || undefined;\n\n  return (\n    <Field id={ids.id} label={label} hint={hint} error={error} required={required} className={className}>\n      <div ref={shakeRef} className={`${fieldSurface({ invalid: !!error, disabled })} ${sm ? \"h-7 text-[12.5px]\" : \"h-9 text-[13.5px]\"}`}>\n        {leading && <span className={`flex shrink-0 items-center text-muted-foreground ${sm ? \"pl-2.5\" : \"pl-3.5\"} [&_svg]:size-3.5`}>{leading}</span>}\n        <input\n          ref={setRefs}\n          id={ids.id}\n          value={value}\n          defaultValue={defaultValue}\n          disabled={disabled}\n          required={required}\n          aria-invalid={error ? true : undefined}\n          aria-describedby={describedBy}\n          onChange={(e) => {\n            if (value === undefined) setText(e.target.value);\n            onChange?.(e);\n            onValueChange?.(e.target.value);\n          }}\n          className={`h-full min-w-0 flex-1 bg-transparent outline-none placeholder:text-muted-foreground/70 ${leading ? \"pl-2\" : sm ? \"pl-3\" : \"pl-4\"} ${trailing || clearable ? \"pr-1\" : sm ? \"pr-3\" : \"pr-4\"} ${inputClassName}`}\n          {...props}\n        />\n        {clearable && (\n          // The clear button folds its own width open, so the text never jumps.\n          <span\n            className=\"grid shrink-0\"\n            style={{ gridTemplateColumns: showClear ? \"1fr\" : \"0fr\", transition: reduced ? \"none\" : `grid-template-columns 320ms ${EASE_OUT}` }}\n          >\n            <span className=\"min-w-0 overflow-hidden\">\n              <button\n                type=\"button\"\n                tabIndex={showClear ? 0 : -1}\n                aria-hidden={!showClear}\n                aria-label=\"Clear\"\n                onClick={() => {\n                  if (!inner.current) return;\n                  setNativeValue(inner.current, \"\");\n                  inner.current.focus();\n                }}\n                className={`mr-1 flex items-center justify-center rounded-full text-muted-foreground cursor-pointer hover:bg-accent hover:text-foreground ${sm ? \"size-5\" : \"size-7\"} ${INTERACTION_FOCUS}`}\n                style={interactionIcon(showClear, reduced)}\n              >\n                <ClearIcon />\n              </button>\n            </span>\n          </span>\n        )}\n        {trailing && <span className={`flex shrink-0 items-center text-muted-foreground ${sm ? \"pr-1\" : \"pr-1.5\"} [&_svg]:size-3.5`}>{trailing}</span>}\n      </div>\n    </Field>\n  );\n}\n\nexport default Input;\n"},{"path":"registry/field.tsx","type":"registry:component","content":"// Hairline UI · input · MIT License\n// Copyright (c) 2026 Hairline UI · https://hairlineui.com/license#components\n\n\"use client\";\n\nimport * as React from \"react\";\nimport { EASE_OUT, useInteractionReducedMotion } from \"./ui-interaction-motion\";\n\n/* ─────────────────────────────────────────────────────────\n * FIELD: the column every text control sits in\n *\n *   label    above, with an optional aside on the right (a count)\n *   hint     under the control while all is well\n *   error    takes the hint's place: the hint folds away, the\n *            error rises in out of a blur, the control shakes once\n *\n * Shared by input, textarea, password, search and select so the\n * whole family has one surface, one focus ring and one error.\n * ───────────────────────────────────────────────────────── */\n\nexport interface FieldProps {\n  id: string;\n  label?: React.ReactNode;\n  /** Right side of the label row, e.g. a character count. */\n  aside?: React.ReactNode;\n  hint?: React.ReactNode;\n  error?: React.ReactNode;\n  required?: boolean;\n  className?: string;\n  children: React.ReactNode;\n}\n\nexport type FieldSize = \"sm\" | \"md\";\n\n/** The surface of a text control: a hairline that darkens on hover and turns to the ring on focus. */\nexport function fieldSurface({ invalid, disabled, round = true }: { invalid?: boolean; disabled?: boolean; round?: boolean }) {\n  const ring = invalid\n    ? \"shadow-[inset_0_0_0_1px_color-mix(in_oklab,var(--color-red-500)_55%,transparent)] focus-within:shadow-[inset_0_0_0_1px_var(--color-red-500),0_0_0_3px_color-mix(in_oklab,var(--color-red-500)_14%,transparent)]\"\n    : \"shadow-[inset_0_0_0_1px_var(--border)] [&:not(:focus-within)]:hover:shadow-[inset_0_0_0_1px_color-mix(in_oklab,var(--foreground)_24%,var(--border))] focus-within:shadow-[inset_0_0_0_1px_var(--ring),0_0_0_3px_color-mix(in_oklab,var(--ring)_16%,transparent)]\";\n  return `relative flex w-full items-center bg-background text-foreground transition-[box-shadow,background-color,opacity] duration-200 ease-[cubic-bezier(0.16,1,0.3,1)] ${round ? \"rounded-full\" : \"rounded-[14px]\"} ${ring} ${\n    disabled ? \"pointer-events-none opacity-50\" : \"\"\n  }`;\n}\n\n/** ids for the control's label, hint and error. */\nexport function useFieldIds(id?: string) {\n  const auto = React.useId();\n  const base = id ?? auto;\n  return { id: base, hint: `${base}-hint`, error: `${base}-error` };\n}\n\n/** One short shake whenever `on` turns true (an error arrives). */\nexport function useShake<T extends HTMLElement>(on: boolean) {\n  const ref = React.useRef<T>(null);\n  const reduced = useInteractionReducedMotion();\n  React.useEffect(() => {\n    if (!on || reduced) return;\n    ref.current?.animate(\n      [{ transform: \"none\" }, { transform: \"translateX(-4px)\" }, { transform: \"translateX(4px)\" }, { transform: \"translateX(-2px)\" }, { transform: \"none\" }],\n      { duration: 380, easing: \"ease-out\" },\n    );\n  }, [on, reduced]);\n  return ref;\n}\n\n/** Sets a native input's value so React's onChange still fires (clear buttons, pickers). */\nexport function setNativeValue(el: HTMLInputElement | HTMLTextAreaElement, value: string) {\n  const proto = el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;\n  Object.getOwnPropertyDescriptor(proto, \"value\")?.set?.call(el, value);\n  el.dispatchEvent(new Event(\"input\", { bubbles: true }));\n}\n\n/** A line under the control that folds open and closed; keeps its last words while closing. */\nfunction Line({ show, children, id, tone, reduced }: { show: boolean; children: React.ReactNode; id: string; tone: string; reduced: boolean }) {\n  const [kept, setKept] = React.useState(children);\n  if (show && children !== kept) setKept(children);\n  return (\n    <div\n      className=\"grid\"\n      style={{ gridTemplateRows: show ? \"1fr\" : \"0fr\", transition: reduced ? \"none\" : `grid-template-rows 320ms ${EASE_OUT}` }}\n    >\n      <div className=\"min-h-0 overflow-hidden\">\n        <p\n          id={id}\n          className={`pt-1.5 pl-3 text-[12.5px] leading-snug ${tone}`}\n          style={{\n            opacity: show ? 1 : 0,\n            filter: show ? \"blur(0px)\" : \"blur(4px)\",\n            transform: show ? \"none\" : \"translateY(-4px)\",\n            transition: reduced ? \"none\" : show ? `opacity 280ms ${EASE_OUT} 60ms, filter 280ms ${EASE_OUT} 60ms, transform 380ms ${EASE_OUT}` : `opacity 140ms ${EASE_OUT}, filter 140ms ${EASE_OUT}, transform 200ms ${EASE_OUT}`,\n          }}\n        >\n          {show ? children : kept}\n        </p>\n      </div>\n    </div>\n  );\n}\n\nexport function Field({ id, label, aside, hint, error, required, className = \"\", children }: FieldProps) {\n  const reduced = useInteractionReducedMotion();\n  const ids = { hint: `${id}-hint`, error: `${id}-error` };\n  return (\n    <div className={`flex w-full flex-col ${className}`}>\n      {(label || aside) && (\n        <div className=\"flex items-baseline justify-between gap-3 px-3 pb-1.5\">\n          {label && (\n            <label htmlFor={id} className=\"text-[13px] font-medium text-foreground\">\n              {label}\n              {required && (\n                <span aria-hidden=\"true\" className=\"ml-0.5 text-muted-foreground\">\n                  *\n                </span>\n              )}\n            </label>\n          )}\n          {aside && <span className=\"text-[12px] tabular-nums text-muted-foreground\">{aside}</span>}\n        </div>\n      )}\n      {children}\n      <Line show={!error && !!hint} id={ids.hint} tone=\"text-muted-foreground\" reduced={reduced}>\n        {hint}\n      </Line>\n      <Line show={!!error} id={ids.error} tone=\"text-red-600 dark:text-red-400\" reduced={reduced}>\n        {error}\n      </Line>\n    </div>\n  );\n}\n\nexport default Field;\n"}],"css":{},"docs":"Docs and live preview: https://hairlineui.com/ui/input. MIT licensed: https://hairlineui.com/license#components","meta":{"license":"MIT"}}