{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "input",
  "title": "Input",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-vue/components/input/Input.vue",
      "content": "<script setup lang=\"ts\">\nimport type { Component, HTMLAttributes } from 'vue'\nimport { computed, ref, useAttrs, useSlots } from 'vue'\nimport { useVModel } from '@vueuse/core'\nimport { cn } from '@/lib/utils'\nimport { X, Eye, EyeOff } from 'lucide-vue-next'\n\ndefineOptions({\n  inheritAttrs: false,\n})\n\ninterface Props {\n  defaultValue?: string | number\n  modelValue?: string | number\n  class?: HTMLAttributes['class']\n  size?: 'small' | 'middle' | 'large'\n  variant?: 'outlined' | 'filled' | 'borderless'\n  status?: 'error' | 'warning'\n  prefix?: string\n  suffix?: string\n  // Convenience props for the very common \"icon at the start/end\" case.\n  // Pass a lucide-vue-next (or any) component: `:prefix-icon=\"Mail\"`.\n  // If both `prefix` (string) and `prefixIcon` are set, the icon wins.\n  prefixIcon?: Component\n  suffixIcon?: Component\n  addonBefore?: string\n  addonAfter?: string\n  allowClear?: boolean\n  showCount?: boolean\n  showPasswordToggle?: boolean\n  disabled?: boolean\n  readonly?: boolean\n  maxlength?: number | string\n  minlength?: number | string\n  type?: string\n  placeholder?: string\n  id?: string\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  size: 'middle',\n  variant: 'outlined',\n  type: 'text',\n})\n\nconst emits = defineEmits<{\n  (e: 'update:modelValue', payload: string | number): void\n}>()\n\nconst slots = useSlots()\nconst attrs = useAttrs()\n\nconst modelValue = useVModel(props, 'modelValue', emits, {\n  passive: true,\n  defaultValue: props.defaultValue,\n})\n\nconst inputRef = ref<HTMLInputElement | null>(null)\nconst focused = ref(false)\nconst hovered = ref(false)\nconst passwordVisible = ref(false)\n\nconst isPassword = computed(() => props.type === 'password')\nconst hasPrefix = computed(() => !!props.prefix || !!props.prefixIcon || !!slots.prefix)\nconst hasSuffix = computed(() => !!props.suffix || !!props.suffixIcon || !!slots.suffix)\nconst hasAddonBefore = computed(() => !!props.addonBefore || !!slots.addonBefore)\nconst hasAddonAfter = computed(() => !!props.addonAfter || !!slots.addonAfter)\n\nconst hasRightConfig = computed(() => {\n  const hasCount = props.showCount && props.maxlength != null\n  const hasPasswordToggle = props.showPasswordToggle && isPassword.value\n  return props.allowClear || hasPasswordToggle || hasCount\n})\n\nconst currentLength = computed(() => String(modelValue.value ?? '').length)\n\nconst showClear = computed(() => {\n  return (\n    props.allowClear && !!modelValue.value && (focused.value || hovered.value) && !props.disabled && !props.readonly\n  )\n})\n\nconst showPasswordToggleBtn = computed(() => {\n  return isPassword.value && props.showPasswordToggle && !props.disabled && !props.readonly\n})\n\nconst showCountDisplay = computed(() => {\n  return props.showCount && props.maxlength != null\n})\n\nconst computedType = computed(() => {\n  if (!isPassword.value) return props.type\n  return passwordVisible.value ? 'text' : 'password'\n})\n\nconst sizeClasses = {\n  small: 'h-8 text-xs',\n  middle: 'h-9 text-base md:text-sm',\n  large: 'h-11 text-base',\n}\n\nconst wrapperRounded = computed(() => {\n  if (hasAddonBefore.value && hasAddonAfter.value) return 'rounded-none'\n  if (hasAddonBefore.value) return 'rounded-l-none rounded-r-md'\n  if (hasAddonAfter.value) return 'rounded-r-none rounded-l-md'\n  return 'rounded-md'\n})\n\nconst wrapperClasses = computed(() => {\n  const base = 'flex w-full items-center gap-1.5 overflow-hidden border transition-[color,box-shadow] outline-none'\n  const sizeClass = sizeClasses[props.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[props.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-[var(--warning)] focus-within:border-[var(--warning)] focus-within:ring-[var(--warning)]/20',\n  }\n  const statusClass = props.status ? statusMap[props.status] : ''\n\n  const focusClass = !props.status ? 'focus-within:border-ring focus-within:ring-ring/50 focus-within:ring-[3px]' : ''\n\n  const disabledClass = props.disabled ? 'pointer-events-none opacity-50 cursor-not-allowed bg-muted/30' : ''\n\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.value,\n    props.class,\n  )\n})\n\nfunction 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[props.size],\n  )\n}\n\nconst inputPadding = computed(() => {\n  const hasLeft = hasPrefix.value\n  const hasRight = hasSuffix.value || hasRightConfig.value\n  const leftPad = props.size === 'small' ? 'pl-2' : props.size === 'large' ? 'pl-3' : 'pl-2.5'\n  const rightPad = props.size === 'small' ? 'pr-2' : props.size === 'large' ? 'pr-3' : 'pr-2.5'\n\n  if (!hasLeft && !hasRight) return cn(leftPad, rightPad)\n  if (hasLeft && !hasRight) return cn('pl-0', rightPad)\n  if (!hasLeft && hasRight) return cn(leftPad, 'pr-0')\n  return 'px-0'\n})\n\nfunction handleClear() {\n  modelValue.value = ''\n  inputRef.value?.focus()\n}\n\nfunction togglePassword() {\n  passwordVisible.value = !passwordVisible.value\n  inputRef.value?.focus()\n}\n</script>\n\n<template>\n  <div class=\"flex w-full\">\n    <!-- Addon before -->\n    <div v-if=\"hasAddonBefore\" :class=\"addonClasses('before')\">\n      <slot name=\"addonBefore\">{{ addonBefore }}</slot>\n    </div>\n\n    <!-- Input wrapper -->\n    <div\n      :class=\"wrapperClasses\"\n      data-uipkge\n      data-slot=\"input\"\n      :aria-invalid=\"attrs['aria-invalid'] as 'true' | 'false' | 'grammar' | 'spelling' | undefined\"\n      @mouseenter=\"hovered = true\"\n      @mouseleave=\"hovered = false\"\n      @click=\"inputRef?.focus()\"\n    >\n      <!-- Prefix -->\n      <span\n        v-if=\"hasPrefix\"\n        class=\"text-muted-foreground pointer-events-none shrink-0 select-none\"\n        :class=\"props.size === 'small' ? 'pl-2' : props.size === 'large' ? 'pl-3' : 'pl-2.5'\"\n      >\n        <slot name=\"prefix\">\n          <component :is=\"prefixIcon\" v-if=\"prefixIcon\" class=\"size-4\" aria-hidden=\"true\" />\n          <template v-else>{{ prefix }}</template>\n        </slot>\n      </span>\n\n      <!-- Native input -->\n      <input\n        :id=\"id\"\n        ref=\"inputRef\"\n        v-model=\"modelValue\"\n        v-bind=\"attrs\"\n        :type=\"computedType\"\n        :disabled=\"disabled\"\n        :readonly=\"readonly\"\n        :maxlength=\"maxlength\"\n        :minlength=\"minlength\"\n        :placeholder=\"placeholder\"\n        :class=\"\n          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        \"\n        @focus=\"focused = true\"\n        @blur=\"focused = false\"\n      />\n\n      <!-- Suffix / Actions -- built-in actions (clear / password toggle /\n           count) render first, then the user's suffix slot so the slotted\n           content is always the rightmost element in the row. -->\n      <div\n        class=\"flex shrink-0 items-center gap-1\"\n        :class=\"props.size === 'small' ? 'pr-2' : props.size === 'large' ? 'pr-3' : 'pr-2.5'\"\n      >\n        <button\n          v-if=\"showClear\"\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          @mousedown.prevent=\"handleClear\"\n          @click=\"handleClear\"\n        >\n          <X class=\"size-4\" aria-hidden=\"true\" />\n        </button>\n\n        <button\n          v-if=\"showPasswordToggleBtn\"\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          @mousedown.prevent=\"togglePassword\"\n          @click=\"togglePassword\"\n        >\n          <Eye v-if=\"passwordVisible\" class=\"size-4\" aria-hidden=\"true\" />\n          <EyeOff v-else class=\"size-4\" aria-hidden=\"true\" />\n        </button>\n\n        <span v-if=\"showCountDisplay\" class=\"text-muted-foreground pointer-events-none text-xs select-none\">\n          {{ currentLength }}/{{ maxlength }}\n        </span>\n\n        <span v-if=\"hasSuffix\" class=\"text-muted-foreground pointer-events-none select-none\">\n          <slot name=\"suffix\">\n            <component :is=\"suffixIcon\" v-if=\"suffixIcon\" class=\"size-4\" aria-hidden=\"true\" />\n            <template v-else>{{ suffix }}</template>\n          </slot>\n        </span>\n      </div>\n    </div>\n\n    <!-- Addon after -->\n    <div v-if=\"hasAddonAfter\" :class=\"addonClasses('after')\">\n      <slot name=\"addonAfter\">{{ addonAfter }}</slot>\n    </div>\n  </div>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/input/Input.vue"
    },
    {
      "path": "packages/registry-vue/components/input/index.ts",
      "content": "export { default as Input } from './Input.vue'\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/input/index.ts"
    }
  ],
  "dependencies": [
    "@vueuse/core",
    "lucide-vue-next"
  ],
  "devDependencies": [],
  "registryDependencies": [],
  "description": "Text input — single-line. Same sizing and ring treatment as the rest of the form primitives. Pair with Label, attach an icon via the `start` / `end` slots, or compose into a search input.",
  "categories": [
    "control"
  ]
}