{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "input",
  "title": "Input",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-svelte/components/input/Input.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { Component, Snippet } from 'svelte'\n  import type { HTMLInputAttributes } from 'svelte/elements'\n\n  export interface InputProps extends Omit<HTMLInputAttributes, 'size' | 'prefix'> {\n    /** Uncontrolled initial value. Ignored once `value` is bound. */\n    defaultValue?: string | number\n    /** Two-way bound value (`bind:value`). Falls back to internal state seeded from `defaultValue`. */\n    value?: string | number\n    size?: 'small' | 'middle' | 'large'\n    variant?: 'outlined' | 'filled' | 'borderless'\n    status?: 'error' | 'warning'\n    prefix?: string\n    suffix?: string\n    /**\n     * Convenience props for the very common \"icon at the start/end\" case.\n     * Pass a `@lucide/svelte` (or any) component: `prefixIcon={Mail}`.\n     * If both `prefix` (string) and `prefixIcon` are set, the icon wins.\n     */\n    prefixIcon?: Component\n    suffixIcon?: Component\n    addonBefore?: string\n    addonAfter?: string\n    allowClear?: boolean\n    showCount?: boolean\n    showPasswordToggle?: boolean\n    /** Custom content rendered in the prefix / suffix / addon slots. Wins over the string + icon props. */\n    prefixSnippet?: Snippet\n    suffixSnippet?: Snippet\n    addonBeforeSnippet?: Snippet\n    addonAfterSnippet?: Snippet\n    /** React-parity alias for `maxlength`. */\n    maxLength?: number\n    /** React-parity alias for `autocomplete`. */\n    autoComplete?: string\n    /** Fires with the new value whenever the user types or clears. */\n    onValueChange?: (value: string) => void\n    /** The native `<input>`, via `bind:ref`. */\n    ref?: HTMLInputElement | null\n  }\n</script>\n\n<script lang=\"ts\">\n  import { cn } from '$lib/utils'\n  import { Eye, EyeOff, X } from '@lucide/svelte'\n\n  let {\n    class: className,\n    defaultValue = '',\n    value = $bindable<string | number | undefined>(undefined),\n    size = 'middle',\n    variant = 'outlined',\n    status,\n    prefix,\n    suffix,\n    prefixIcon,\n    suffixIcon,\n    addonBefore,\n    addonAfter,\n    allowClear = false,\n    showCount = false,\n    showPasswordToggle = false,\n    disabled = false,\n    readonly = false,\n    maxlength,\n    maxLength,\n    minlength,\n    autocomplete,\n    autoComplete,\n    type = 'text',\n    placeholder,\n    id,\n    prefixSnippet,\n    suffixSnippet,\n    addonBeforeSnippet,\n    addonAfterSnippet,\n    onValueChange,\n    ref = $bindable(null),\n    ...restProps\n  }: InputProps = $props()\n\n  // svelte-ignore state_referenced_locally: defaultValue seeds uncontrolled state once by design.\n  let uncontrolled = $state<string | number>(defaultValue)\n  // Controlled when the parent binds `value` (including an empty string);\n  // otherwise mirror the internal state seeded from `defaultValue`.\n  const current = $derived(value ?? uncontrolled)\n\n  let focused = $state(false)\n  let hovered = $state(false)\n  let passwordVisible = $state(false)\n\n  const isPassword = $derived(type === 'password')\n  const resolvedMaxLength = $derived(maxLength ?? maxlength)\n  const resolvedAutoComplete = $derived(autoComplete ?? autocomplete)\n  const hasPrefix = $derived(!!prefix || !!prefixIcon || !!prefixSnippet)\n  const hasSuffix = $derived(!!suffix || !!suffixIcon || !!suffixSnippet)\n  const hasAddonBefore = $derived(!!addonBefore || !!addonBeforeSnippet)\n  const hasAddonAfter = $derived(!!addonAfter || !!addonAfterSnippet)\n\n  const hasRightConfig = $derived.by(() => {\n    const hasCount = showCount && resolvedMaxLength != null\n    const hasPasswordToggle = showPasswordToggle && isPassword\n    return allowClear || hasPasswordToggle || !!hasCount\n  })\n\n  const hasRight = $derived(hasSuffix || hasRightConfig)\n  const currentLength = $derived(String(current ?? '').length)\n\n  const showClear = $derived(\n    allowClear && !!current && (focused || hovered) && !disabled && !readonly,\n  )\n  const showPasswordToggleBtn = $derived(isPassword && showPasswordToggle && !disabled && !readonly)\n  const showCountDisplay = $derived(showCount && maxlength != null)\n  const computedType = $derived(!isPassword ? type : passwordVisible ? 'text' : 'password')\n\n  const sizeClasses = {\n    small: 'h-8 text-xs',\n    middle: 'h-9 text-base md:text-sm',\n    large: 'h-11 text-base',\n  }\n\n  const wrapperRounded = $derived.by(() => {\n    if (hasAddonBefore && hasAddonAfter) return 'rounded-none'\n    if (hasAddonBefore) return 'rounded-l-none rounded-r-md'\n    if (hasAddonAfter) return 'rounded-r-none rounded-l-md'\n    return 'rounded-md'\n  })\n\n  const resolvedAriaInvalid = $derived.by(() => {\n    if (status === 'error') return true as const\n    const raw = restProps['aria-invalid'] as boolean | 'true' | 'false' | 'grammar' | 'spelling' | '' | undefined\n    if (raw === true || raw === 'true' || raw === '' || raw === 'grammar' || raw === 'spelling') {\n      return raw === '' ? true : raw\n    }\n    if (raw === false || raw === 'false') return false as const\n    return undefined\n  })\n\n  const wrapperClasses = $derived.by(() => {\n    const base = 'flex w-full items-center gap-1.5 overflow-hidden border transition-[color,box-shadow] outline-none'\n    const sizeClass = sizeClasses[size]\n\n    const variantMap = {\n      outlined: 'border-input bg-transparent shadow-xs',\n      filled: 'border-transparent bg-muted/50 shadow-none',\n      borderless: 'border-transparent bg-transparent shadow-none',\n    }\n    const variantClass = variantMap[variant]\n\n    const statusMap = {\n      error:\n        'border-destructive focus-within:border-destructive focus-within:ring-destructive/20 dark:focus-within:ring-destructive/40',\n      warning: 'border-warning focus-within:border-warning focus-within:ring-warning/20',\n    }\n    const statusClass = status ? statusMap[status] : ''\n    const focusClass = !status ? 'focus-within:border-ring focus-within:ring-ring/50 focus-within:ring-[3px]' : ''\n    const disabledClass = disabled ? 'pointer-events-none opacity-50 cursor-not-allowed bg-muted/30' : ''\n    const ariaInvalidClass =\n      'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive'\n\n    return cn(\n      base,\n      sizeClass,\n      variantClass,\n      statusClass,\n      focusClass,\n      disabledClass,\n      ariaInvalidClass,\n      wrapperRounded,\n      className,\n    )\n  })\n\n  function addonClasses(position: 'before' | 'after') {\n    const roundedClass =\n      position === 'before' ? 'rounded-l-md rounded-r-none border-r-0' : 'rounded-r-md rounded-l-none border-l-0'\n    return cn(\n      'flex items-center bg-muted px-3 text-sm text-muted-foreground border border-input',\n      roundedClass,\n      sizeClasses[size],\n    )\n  }\n\n  const inputPadding = $derived.by(() => {\n    const leftPad = size === 'small' ? 'pl-2' : size === 'large' ? 'pl-3' : 'pl-2.5'\n    const rightPad = size === 'small' ? 'pr-2' : size === 'large' ? 'pr-3' : 'pr-2.5'\n\n    if (!hasPrefix && !hasRight) return cn(leftPad, rightPad)\n    if (hasPrefix && !hasRight) return cn('pl-0', rightPad)\n    if (!hasPrefix && hasRight) return cn(leftPad, 'pr-0')\n    return 'px-0'\n  })\n\n  function setValue(next: string) {\n    value = next\n    uncontrolled = next\n    onValueChange?.(next)\n  }\n\n  function handleInput(e: Event & { currentTarget: HTMLInputElement }) {\n    setValue(e.currentTarget.value)\n  }\n\n  function handleClear() {\n    setValue('')\n    ref?.focus()\n  }\n\n  function togglePassword() {\n    passwordVisible = !passwordVisible\n    ref?.focus()\n  }\n</script>\n\n<div class=\"flex w-full\">\n  <!-- Addon before -->\n  {#if hasAddonBefore}\n    <div class={addonClasses('before')}>\n      {#if addonBeforeSnippet}\n        {@render addonBeforeSnippet()}\n      {:else}\n        {addonBefore}\n      {/if}\n    </div>\n  {/if}\n\n  <!-- Input wrapper -->\n  <!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions: wrapper click only forwards focus to the native input -->\n  <div\n    class={wrapperClasses}\n    data-uipkge=\"\"\n    data-slot=\"input\"\n    aria-invalid={resolvedAriaInvalid}\n    onmouseenter={() => (hovered = true)}\n    onmouseleave={() => (hovered = false)}\n    onclick={() => ref?.focus()}\n  >\n    <!-- Prefix -->\n    {#if hasPrefix}\n      <span\n        class=\"text-muted-foreground pointer-events-none shrink-0 select-none {size === 'small'\n          ? 'pl-2'\n          : size === 'large'\n            ? 'pl-3'\n            : 'pl-2.5'}\"\n      >\n        {#if prefixSnippet}\n          {@render prefixSnippet()}\n        {:else if prefixIcon}\n          {@const PrefixIcon = prefixIcon}\n          <PrefixIcon class=\"size-4\" aria-hidden=\"true\" />\n        {:else}\n          {prefix}\n        {/if}\n      </span>\n    {/if}\n\n    <!-- Native input -->\n    <input\n      {id}\n      bind:this={ref}\n      value={current}\n      {...restProps}\n      type={computedType}\n      {disabled}\n      {readonly}\n      maxlength={resolvedMaxLength}\n      autocomplete={resolvedAutoComplete as any}\n      {minlength}\n      {placeholder}\n      aria-invalid={resolvedAriaInvalid}\n      class={cn(\n        'w-full min-w-0 flex-1 bg-transparent outline-none',\n        'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground',\n        'file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium',\n        'disabled:cursor-not-allowed',\n        inputPadding,\n      )}\n      oninput={handleInput}\n      onfocus={() => (focused = true)}\n      onblur={() => (focused = false)}\n    />\n\n    <!-- Suffix / Actions -- only render when there is something on the\n         right; otherwise an empty padded flex child steals horizontal\n         space from plain inputs. Built-in actions (clear / password\n         toggle / count) render first, then the user's suffix so the\n         slotted content is always the rightmost element in the row. -->\n    {#if hasRight}\n      <div\n        class=\"flex shrink-0 items-center gap-1 {size === 'small' ? 'pr-2' : size === 'large' ? 'pr-3' : 'pr-2.5'}\"\n      >\n        {#if showClear}\n          <button\n            type=\"button\"\n            aria-label=\"Clear input\"\n            class=\"text-muted-foreground hover:text-foreground focus-visible:ring-ring/50 shrink-0 rounded p-0.5 transition-colors focus-visible:ring-1 focus-visible:outline-none\"\n            onmousedown={(e) => e.preventDefault()}\n            onclick={handleClear}\n          >\n            <X class=\"size-4\" aria-hidden=\"true\" />\n          </button>\n        {/if}\n\n        {#if showPasswordToggleBtn}\n          <button\n            type=\"button\"\n            aria-label={passwordVisible ? 'Hide password' : 'Show password'}\n            aria-pressed={passwordVisible}\n            class=\"text-muted-foreground hover:text-foreground focus-visible:ring-ring/50 shrink-0 rounded p-0.5 transition-colors focus-visible:ring-1 focus-visible:outline-none\"\n            onmousedown={(e) => e.preventDefault()}\n            onclick={togglePassword}\n          >\n            {#if passwordVisible}\n              <EyeOff class=\"size-4\" aria-hidden=\"true\" />\n            {:else}\n              <Eye class=\"size-4\" aria-hidden=\"true\" />\n            {/if}\n          </button>\n        {/if}\n\n        {#if showCountDisplay}\n          <span class=\"text-muted-foreground pointer-events-none text-xs select-none\">\n            {currentLength}/{resolvedMaxLength}\n          </span>\n        {/if}\n\n        {#if hasSuffix}\n          <span class=\"text-muted-foreground pointer-events-none select-none\">\n            {#if suffixSnippet}\n              {@render suffixSnippet()}\n            {:else if suffixIcon}\n              {@const SuffixIcon = suffixIcon}\n              <SuffixIcon class=\"size-4\" aria-hidden=\"true\" />\n            {:else}\n              {suffix}\n            {/if}\n          </span>\n        {/if}\n      </div>\n    {/if}\n  </div>\n\n  <!-- Addon after -->\n  {#if hasAddonAfter}\n    <div class={addonClasses('after')}>\n      {#if addonAfterSnippet}\n        {@render addonAfterSnippet()}\n      {:else}\n        {addonAfter}\n      {/if}\n    </div>\n  {/if}\n</div>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/input/Input.svelte"
    },
    {
      "path": "packages/registry-svelte/components/input/InputGroup.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { Snippet } from 'svelte'\n  import type { HTMLAttributes } from 'svelte/elements'\n\n  export interface InputGroupProps extends HTMLAttributes<HTMLDivElement> {\n    size?: 'small' | 'middle' | 'large'\n    disabled?: boolean\n    children?: Snippet\n    ref?: HTMLDivElement | null\n  }\n</script>\n\n<script lang=\"ts\">\n  import { cn } from '$lib/utils'\n\n  let {\n    class: className,\n    size = 'middle',\n    disabled = false,\n    children,\n    ref = $bindable(null),\n    ...restProps\n  }: InputGroupProps = $props()\n\n  const sizeClasses: Record<string, string> = {\n    small: 'h-8 text-xs',\n    middle: 'h-9 text-sm',\n    large: 'h-11 text-base',\n  }\n</script>\n\n<div\n  bind:this={ref}\n  data-uipkge=\"\"\n  data-slot=\"input-group\"\n  data-size={size}\n  data-disabled={disabled ? '' : undefined}\n  class={cn(\n    'group/input-group border-input bg-background relative flex w-full items-stretch rounded-md border shadow-xs transition-[color,box-shadow]',\n    'focus-within:border-ring focus-within:ring-ring/50 focus-within:ring-[3px] focus-within:outline-none',\n    '[&_[data-slot=input]]:rounded-none [&_[data-slot=input]]:border-0 [&_[data-slot=input]]:bg-transparent [&_[data-slot=input]]:shadow-none [&_[data-slot=input]]:focus-within:ring-0',\n    '[&_input]:h-full [&_input]:flex-1 [&_input]:border-0 [&_input]:bg-transparent [&_input]:px-3 [&_input]:text-sm [&_input]:outline-none [&_input]:focus-visible:ring-0',\n    disabled && 'bg-muted/30 pointer-events-none cursor-not-allowed opacity-50',\n    sizeClasses[size],\n    className,\n  )}\n  {...restProps}\n>\n  {@render children?.()}\n</div>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/input/InputGroup.svelte"
    },
    {
      "path": "packages/registry-svelte/components/input/InputGroupAddon.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { Snippet } from 'svelte'\n  import type { HTMLAttributes } from 'svelte/elements'\n\n  export interface InputGroupAddonProps extends HTMLAttributes<HTMLDivElement> {\n    align?: 'inline' | 'block'\n    children?: Snippet\n    ref?: HTMLDivElement | null\n  }\n</script>\n\n<script lang=\"ts\">\n  import { cn } from '$lib/utils'\n\n  let {\n    class: className,\n    align = 'inline',\n    children,\n    ref = $bindable(null),\n    ...restProps\n  }: InputGroupAddonProps = $props()\n</script>\n\n<div\n  bind:this={ref}\n  data-uipkge=\"\"\n  data-slot=\"input-group-addon\"\n  data-align={align}\n  class={cn(\n    'text-muted-foreground flex shrink-0 items-center justify-center px-3 text-sm select-none',\n    'border-input first:rounded-l-[calc(var(--radius)-1px)] last:rounded-r-[calc(var(--radius)-1px)]',\n    'border-r first:border-l-0 last:border-r-0',\n    className,\n  )}\n  {...restProps}\n>\n  {@render children?.()}\n</div>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/input/InputGroupAddon.svelte"
    },
    {
      "path": "packages/registry-svelte/components/input/InputGroupButton.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { Snippet } from 'svelte'\n  import type { HTMLButtonAttributes } from 'svelte/elements'\n\n  export interface InputGroupButtonProps extends HTMLButtonAttributes {\n    variant?: 'default' | 'secondary' | 'ghost' | 'outline'\n    children?: Snippet\n    ref?: HTMLButtonElement | null\n  }\n</script>\n\n<script lang=\"ts\">\n  import { cn } from '$lib/utils'\n\n  let {\n    class: className,\n    variant = 'ghost',\n    type = 'button',\n    disabled,\n    children,\n    ref = $bindable(null),\n    ...restProps\n  }: InputGroupButtonProps = $props()\n\n  const variantClasses: Record<string, string> = {\n    default: 'bg-primary text-primary-foreground hover:bg-primary/90',\n    secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',\n    ghost: 'hover:bg-accent hover:text-accent-foreground',\n    outline: 'border-l border-input hover:bg-accent hover:text-accent-foreground',\n  }\n</script>\n\n<button\n  bind:this={ref}\n  {type}\n  data-uipkge=\"\"\n  data-slot=\"input-group-button\"\n  data-variant={variant}\n  {disabled}\n  class={cn(\n    'inline-flex shrink-0 cursor-pointer items-center justify-center gap-1.5 px-3 text-sm font-medium transition-colors select-none',\n    'first:rounded-l-[calc(var(--radius)-1px)] last:rounded-r-[calc(var(--radius)-1px)]',\n    'focus-visible:ring-ring focus-visible:ring-1 focus-visible:outline-none',\n    'disabled:pointer-events-none disabled:opacity-50',\n    variantClasses[variant],\n    className,\n  )}\n  {...restProps}\n>\n  {@render children?.()}\n</button>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/input/InputGroupButton.svelte"
    },
    {
      "path": "packages/registry-svelte/components/input/index.ts",
      "content": "export { default as Input, type InputProps } from './Input.svelte'\nexport { default as InputGroup, type InputGroupProps } from './InputGroup.svelte'\nexport { default as InputGroupAddon, type InputGroupAddonProps } from './InputGroupAddon.svelte'\nexport { default as InputGroupButton, type InputGroupButtonProps } from './InputGroupButton.svelte'\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/input/index.ts"
    }
  ],
  "dependencies": [
    "@lucide/svelte"
  ],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Text input — single-line. Three sizes, three variants (outlined / filled / borderless), error / warning status, prefix / suffix (string, icon, or snippet), addonBefore / addonAfter, allow-clear, password toggle, char count, and composite InputGroup with addons and action buttons.",
  "categories": [
    "control"
  ]
}