{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"input","type":"registry:ui","title":"Input","description":"Text field on a sunken well in three sizes with a volt focus halo, leading icon, joined text, select and button add-ons, a help tooltip, a clear button and a single shake when an error appears.","author":"GetUrUI <hello@geturui.dev>","dependencies":["motion","lucide-react"],"registryDependencies":["@geturui/field","@geturui/tooltip"],"files":[{"path":"registry/geturui/ui/input.tsx","type":"registry:ui","target":"components/ui/input.tsx","content":"\"use client\";\n\nimport * as React from \"react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { AlertCircle, ChevronDown, CircleHelp, X } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport { Field, fieldWellClasses, type FieldSize } from \"@/components/ui/field\";\nimport { Tooltip } from \"@/components/ui/tooltip\";\n\ntype IconComponent = React.ComponentType<{ className?: string; \"aria-hidden\"?: boolean | \"true\" | \"false\" }>;\n\nexport interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, \"size\"> {\n  /** Height: 32, 40 or 48px, matching Button. @default \"md\" */\n  size?: FieldSize;\n  /** Leading icon node inside the field. */\n  icon?: React.ReactNode;\n  /** Leading icon as a component (`Mail`) or node; sized to the field. */\n  iconLeading?: IconComponent | React.ReactNode;\n  /** Keyboard shortcut chip at the end of the field. */\n  kbd?: string;\n  label?: React.ReactNode;\n  hint?: React.ReactNode;\n  /** Error text. Replaces the hint, turns the border red and shakes once when it appears. */\n  error?: React.ReactNode;\n  /** Content inside the field after the input, such as a status icon or a small button. */\n  trailing?: React.ReactNode;\n  /** Joined segment before the field. A string renders a text add-on (\"https://\"); pass InputAddonSelect or InputAddonButton for controls. */\n  leadingAddon?: React.ReactNode;\n  /** Joined segment after the field. */\n  trailingAddon?: React.ReactNode;\n  /** Help text behind a small question-mark button on the label row. */\n  tooltip?: React.ReactNode;\n  /** Right side of the label row: \"Optional\", a counter, a link. */\n  labelAside?: React.ReactNode;\n  /** Show a clear button while the field has a value. */\n  clearable?: boolean;\n  /** Called after the clear button empties the field. onChange also fires with an empty value. */\n  onClear?: () => void;\n  /** Classes for the outer grid (label, field, hint). */\n  wrapperClassName?: string;\n  /** Classes for the <input> itself. */\n  inputClassName?: string;\n}\n\nconst iconSize: Record<FieldSize, string> = { sm: \"size-3.5\", md: \"size-4\", lg: \"size-[18px]\" };\n\nfunction isComponent(icon: unknown): icon is IconComponent {\n  if (typeof icon === \"function\") return true;\n  return typeof icon === \"object\" && icon !== null && !React.isValidElement(icon) && \"$$typeof\" in icon;\n}\n\nfunction Addon({ side, children }: { side: \"leading\" | \"trailing\"; children: React.ReactNode }) {\n  const text = typeof children === \"string\" || typeof children === \"number\";\n  return (\n    <span\n      className={cn(\n        \"flex shrink-0 self-stretch overflow-hidden border-border-strong\",\n        side === \"leading\" ? \"rounded-l-[inherit] border-r\" : \"rounded-r-[inherit] border-l\",\n        text ? \"items-center bg-surface px-3 text-ink-muted\" : \"items-stretch\",\n      )}\n    >\n      {children}\n    </span>\n  );\n}\n\n/**\n * Text field on a sunken well: three sizes, leading icon, joined add-ons, help tooltip, clear button,\n * volt halo on focus and one shake on error.\n */\nexport const Input = React.forwardRef<HTMLInputElement, InputProps>(function Input(\n  {\n    size = \"md\",\n    icon,\n    iconLeading,\n    kbd,\n    label,\n    hint,\n    error,\n    trailing,\n    leadingAddon,\n    trailingAddon,\n    tooltip,\n    labelAside,\n    clearable,\n    onClear,\n    className,\n    wrapperClassName,\n    inputClassName,\n    id,\n    disabled,\n    required,\n    readOnly,\n    value,\n    defaultValue,\n    onChange,\n    \"aria-describedby\": describedByProp,\n    ...props\n  },\n  ref,\n) {\n  const reduce = useReducedMotion();\n  const innerRef = React.useRef<HTMLInputElement>(null);\n  React.useImperativeHandle(ref, () => innerRef.current as HTMLInputElement, []);\n  const [filledInner, setFilledInner] = React.useState(() => String(defaultValue ?? \"\").length > 0);\n  const filled = value !== undefined && value !== null ? String(value).length > 0 : filledInner;\n  const showClear = clearable && filled && !disabled && !readOnly;\n  const s = iconSize[size];\n\n  const lead =\n    iconLeading != null && iconLeading !== false\n      ? isComponent(iconLeading)\n        ? React.createElement(iconLeading, { className: cn(s, \"shrink-0\"), \"aria-hidden\": true })\n        : <span className={cn(\"inline-flex shrink-0 [&>svg]:size-full\", s)}>{iconLeading as React.ReactNode}</span>\n      : icon;\n\n  function clear() {\n    const el = innerRef.current;\n    if (!el) return;\n    // Go through the native setter so React's onChange fires for controlled and uncontrolled fields alike.\n    Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, \"value\")?.set?.call(el, \"\");\n    el.dispatchEvent(new Event(\"input\", { bubbles: true }));\n    setFilledInner(false);\n    el.focus();\n    onClear?.();\n  }\n\n  const help = tooltip ? (\n    <Tooltip content={tooltip} side=\"top\" arrow>\n      <button\n        type=\"button\"\n        aria-label=\"More info\"\n        className=\"grid size-4 place-items-center rounded-full text-ink-subtle outline-none transition-colors hover:text-ink focus-visible:shadow-[var(--focus-ring)]\"\n      >\n        <CircleHelp className=\"size-3.5\" aria-hidden />\n      </button>\n    </Tooltip>\n  ) : null;\n  const aside =\n    labelAside || help ? (\n      <span className=\"inline-flex items-center gap-2\">\n        {labelAside}\n        {help}\n      </span>\n    ) : undefined;\n\n  return (\n    <Field label={label} hint={hint} error={error} required={required} labelAside={aside} id={id} disabled={disabled} className={wrapperClassName}>\n      {(ids) => (\n        <div\n          className={fieldWellClasses({\n            size,\n            invalid: ids.invalid,\n            disabled,\n            className: cn(leadingAddon != null && \"pl-0\", trailingAddon != null && \"pr-0\", className),\n          })}\n        >\n          {leadingAddon != null && <Addon side=\"leading\">{leadingAddon}</Addon>}\n          {lead}\n          <input\n            ref={innerRef}\n            id={ids.id}\n            disabled={disabled}\n            required={required}\n            readOnly={readOnly}\n            value={value}\n            defaultValue={defaultValue}\n            onChange={(e) => {\n              setFilledInner(e.target.value.length > 0);\n              onChange?.(e);\n            }}\n            aria-invalid={ids.invalid || undefined}\n            aria-describedby={[ids.describedBy, describedByProp].filter(Boolean).join(\" \") || undefined}\n            {...props}\n            className={cn(\n              \"min-w-0 flex-1 self-stretch bg-transparent text-ink outline-none focus-visible:shadow-none placeholder:text-ink-subtle disabled:cursor-not-allowed\",\n              \"[&::-webkit-search-cancel-button]:hidden\",\n              inputClassName,\n            )}\n          />\n          <AnimatePresence initial={false}>\n            {showClear && (\n              <motion.button\n                key=\"clear\"\n                type=\"button\"\n                aria-label=\"Clear\"\n                onClick={clear}\n                initial={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.5, filter: \"blur(4px)\" }}\n                animate={{ opacity: 1, scale: 1, filter: \"blur(0px)\" }}\n                exit={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.5, filter: \"blur(4px)\" }}\n                transition={{ type: \"spring\", stiffness: 520, damping: 28 }}\n                className=\"-mr-0.5 grid size-5 shrink-0 place-items-center rounded-full text-ink-subtle outline-none transition-colors hover:bg-surface-hover hover:text-ink focus-visible:shadow-[var(--focus-ring)]\"\n              >\n                <X className=\"size-3.5\" aria-hidden />\n              </motion.button>\n            )}\n          </AnimatePresence>\n          {ids.invalid && trailing == null && <AlertCircle aria-hidden className={cn(s, \"shrink-0 text-danger\")} />}\n          {trailing}\n          {kbd && (\n            <kbd\n              aria-hidden\n              className=\"inline-flex h-5 min-w-5 shrink-0 items-center justify-center rounded-md border border-border bg-surface px-1 font-mono text-[11px] text-ink-muted\"\n            >\n              {kbd}\n            </kbd>\n          )}\n          {trailingAddon != null && <Addon side=\"trailing\">{trailingAddon}</Addon>}\n        </div>\n      )}\n    </Field>\n  );\n});\n\nexport interface InputAddonSelectProps extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, \"children\"> {\n  options: { value: string; label: string }[];\n  /** Required: names the select for screen readers (\"Country\", \"Currency\"). */\n  \"aria-label\": string;\n}\n\n/** Native select styled as a joined Input add-on (country code, currency, protocol). */\nexport function InputAddonSelect({ options, className, ...props }: InputAddonSelectProps) {\n  return (\n    <span className=\"relative flex\">\n      <select\n        {...props}\n        className={cn(\n          \"cursor-pointer appearance-none bg-transparent pl-3 pr-7 text-ink outline-none transition-colors hover:bg-surface-hover focus-visible:bg-surface-hover focus-visible:shadow-none\",\n          className,\n        )}\n      >\n        {options.map((o) => (\n          <option key={o.value} value={o.value}>\n            {o.label}\n          </option>\n        ))}\n      </select>\n      <ChevronDown aria-hidden className=\"pointer-events-none absolute right-2 top-1/2 size-3.5 -translate-y-1/2 text-ink-subtle\" />\n    </span>\n  );\n}\n\n/** Flat button styled as a joined Input add-on (\"Copy\", \"Apply\"). */\nexport const InputAddonButton = React.forwardRef<HTMLButtonElement, React.ButtonHTMLAttributes<HTMLButtonElement>>(function InputAddonButton(\n  { className, type = \"button\", ...props },\n  ref,\n) {\n  return (\n    <button\n      ref={ref}\n      type={type}\n      {...props}\n      className={cn(\n        \"inline-flex items-center gap-1.5 bg-surface px-3 font-medium text-ink outline-none transition-colors hover:bg-surface-hover focus-visible:bg-surface-hover focus-visible:shadow-[inset_0_0_0_2px_var(--volt-ink)] disabled:pointer-events-none disabled:opacity-45 [&>svg]:size-4\",\n        className,\n      )}\n    />\n  );\n});\n"},{"path":"LICENSE-geturui.md","type":"registry:file","target":"LICENSE-geturui.md","content":"MIT License\n\nCopyright (c) 2026 geturui.com\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"}],"categories":["components","input","text field","form","validation","error","search","add-on","input group"],"meta":{"tier":"free","license":"MIT","docs":"https://geturui.com/docs/input"}}