{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "textarea",
  "title": "Textarea",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-vue/components/textarea/Textarea.vue",
      "content": "<script setup lang=\"ts\">\nimport type { HTMLAttributes } from 'vue'\nimport { provide, computed, ref, nextTick, watch, onMounted } from 'vue'\nimport { useId } from 'reka-ui'\nimport { useVModel } from '@vueuse/core'\nimport { cn } from '@/lib/utils'\nimport { Loader, Check, AlertCircle, X } from 'lucide-vue-next'\nimport { Label } from '@/components/ui/label'\n\nexport interface TextareaProps {\n  // Core\n  modelValue?: string | number\n  defaultValue?: string | number\n  label?: string\n  placeholder?: string\n  hint?: string\n  error?: string\n  success?: string\n  messages?: string[]\n  disabled?: boolean\n  readonly?: boolean\n  required?: boolean\n  autofocus?: boolean\n  name?: string\n  id?: string\n\n  // Variants (Vuetify-style)\n  variant?: 'outlined' | 'filled' | 'solo' | 'underlined' | 'plain'\n  color?: 'primary' | 'secondary' | 'error' | 'warning' | 'info' | 'success'\n  density?: 'compact' | 'comfortable' | 'default'\n\n  // Appearance\n  rounded?: 'none' | 'sm' | 'md' | 'lg' | 'xl' | 'pill' | 'circle' | 'full'\n\n  // Auto size (Ant Design API)\n  autoSize?: boolean | { minRows?: number; maxRows?: number }\n\n  // Legacy auto grow / resize\n  autoGrow?: boolean\n  noResize?: boolean\n  autoResize?: boolean\n\n  // Rows -- accept both number and string (\"3\" vs :rows=\"3\") so unbound\n  // attribute usage doesn't trip the Vue prop-type warning.\n  rows?: number | string\n  rowHeight?: number\n\n  // Prefix/Suffix\n  prefix?: string\n  suffix?: string\n\n  // Counter (legacy)\n  counter?: boolean | number\n\n  // Show count (Ant Design API)\n  showCount?: boolean | { formatter?: (count: number, maxLength?: number) => string }\n\n  // Max length\n  maxLength?: number\n\n  // Allow clear\n  allowClear?: boolean\n\n  // Validation\n  rules?: Array<(value: any) => true | string>\n  errorMessages?: string | string[]\n  successMessages?: string | string[]\n  validateOn?: 'blur' | 'input' | 'submit' | 'lazy' | 'blurlazy' | 'inputlazy'\n\n  // States\n  loading?: boolean\n  persistentHint?: boolean\n  persistentError?: boolean\n  persistentPlaceholder?: boolean\n  persistentPrefix?: boolean\n  persistentSuffix?: boolean\n\n  // Misc\n  class?: HTMLAttributes['class']\n  inputClass?: HTMLAttributes['class']\n  labelClass?: HTMLAttributes['class']\n  hintClass?: HTMLAttributes['class']\n  bgColor?: string\n  flat?: boolean\n  bordered?: boolean\n\n  // Browser\n  spellcheck?: boolean\n  autocomplete?: string\n\n  // Direction\n  direction?: 'ltr' | 'rtl'\n}\n\nconst props = withDefaults(defineProps<TextareaProps>(), {\n  variant: 'outlined',\n  density: 'default',\n  rounded: 'none',\n  rows: 3,\n  rowHeight: 24,\n  autoGrow: false,\n  noResize: false,\n  autoResize: false,\n  flat: false,\n  bordered: true,\n  persistentHint: false,\n  persistentError: false,\n  persistentPlaceholder: false,\n  direction: 'ltr',\n})\n\nconst emits = defineEmits<{\n  (e: 'update:modelValue', payload: string | number): void\n  (e: 'click:clear'): void\n  (e: 'focus'): void\n  (e: 'blur'): void\n  (e: 'keydown'): void\n  (e: 'keyup'): void\n}>()\n\nconst textareaId = props.id ?? `textarea-${useId()}`\nconst descriptionId = `${textareaId}-description`\nconst messageId = `${textareaId}-message`\n\nprovide('form-item', {\n  id: textareaId,\n  descriptionId,\n  messageId,\n})\n\n// Internal state\nconst internalValue = useVModel(props, 'modelValue', emits, {\n  passive: true,\n  defaultValue: props.defaultValue,\n})\nconst focused = ref(false)\nconst internalErrorMessages = ref<string[]>([])\nconst validated = ref(false)\nconst textareaRef = ref<HTMLTextAreaElement | null>(null)\n\n// Auto size\nconst autoSizeEnabled = computed(() => props.autoSize !== undefined)\nconst anyAutoResize = computed(() => autoSizeEnabled.value || props.autoResize || props.autoGrow)\n\nconst autoSizeConfig = computed<{ minRows?: number; maxRows?: number }>(() => {\n  if (typeof props.autoSize === 'object') {\n    return props.autoSize\n  }\n  return {}\n})\n\nconst minHeightPx = ref(0)\nconst maxHeightPx = ref(Infinity)\n\nconst measureHeights = () => {\n  if (!textareaRef.value) return\n  if (!autoSizeEnabled.value) return\n\n  const el = textareaRef.value\n  const originalValue = el.value\n  const originalRows = el.rows\n  const originalOverflow = el.style.overflowY\n\n  el.value = ''\n  el.style.overflowY = 'hidden'\n\n  const { minRows, maxRows } = autoSizeConfig.value\n\n  if (minRows) {\n    el.rows = minRows\n    minHeightPx.value = el.scrollHeight\n  } else {\n    minHeightPx.value = 0\n  }\n\n  if (maxRows) {\n    el.rows = maxRows\n    maxHeightPx.value = el.scrollHeight\n  } else {\n    maxHeightPx.value = Infinity\n  }\n\n  el.value = originalValue\n  el.rows = originalRows\n  el.style.overflowY = originalOverflow\n\n  autoResize()\n}\n\nwatch(() => autoSizeConfig.value, measureHeights, { deep: true })\n\n// Auto grow functionality (legacy)\nconst rowsNum = computed(() => Number(props.rows) || 3)\n\nconst computedRows = computed(() => {\n  if (autoSizeEnabled.value || props.autoResize) return rowsNum.value\n  if (!props.autoGrow) return rowsNum.value\n  if (!textareaRef.value) return rowsNum.value\n\n  const lineHeight = props.rowHeight\n  const computedHeight = textareaRef.value.scrollHeight\n  const newRows = Math.ceil((computedHeight - lineHeight) / lineHeight) + 1\n  return Math.max(rowsNum.value, newRows)\n})\n\n// Validation\nconst validate = () => {\n  if (!props.rules || props.rules.length === 0) return true\n  internalErrorMessages.value = []\n  for (const rule of props.rules) {\n    const result = rule(internalValue.value)\n    if (result !== true) {\n      internalErrorMessages.value.push(result as string)\n    }\n  }\n  return internalErrorMessages.value.length === 0\n}\n\n// Handle input\nconst handleInput = (e: Event) => {\n  const target = e.target as HTMLTextAreaElement\n  internalValue.value = target.value\n\n  if (anyAutoResize.value) {\n    autoResize()\n  }\n}\n\nconst autoResize = () => {\n  if (!textareaRef.value) return\n  if (!anyAutoResize.value) return\n\n  const el = textareaRef.value\n\n  el.style.height = 'auto'\n  let newHeight = el.scrollHeight\n\n  if (minHeightPx.value && newHeight < minHeightPx.value) {\n    newHeight = minHeightPx.value\n  }\n\n  if (newHeight > maxHeightPx.value) {\n    newHeight = maxHeightPx.value\n    el.style.overflowY = 'auto'\n  } else {\n    el.style.overflowY = 'hidden'\n  }\n\n  el.style.height = `${newHeight}px`\n}\n\n// Handle clear\nconst handleClear = () => {\n  internalValue.value = ''\n  emits('click:clear')\n  nextTick(() => {\n    autoResize()\n    textareaRef.value?.focus()\n  })\n}\n\n// Handle focus/blur\nconst handleFocus = () => {\n  focused.value = true\n  emits('focus')\n}\n\nconst handleBlur = () => {\n  focused.value = false\n  if (props.validateOn === 'blur' || props.validateOn === 'blurlazy') {\n    validate()\n  }\n  emits('blur')\n}\n\n// Compute error/success messages\nconst computedErrorMessages = computed(() => {\n  if (props.errorMessages) {\n    return Array.isArray(props.errorMessages) ? props.errorMessages : [props.errorMessages]\n  }\n  if (props.error) {\n    return [props.error]\n  }\n  return internalErrorMessages.value\n})\n\nconst computedSuccessMessages = computed(() => {\n  if (props.successMessages) {\n    return Array.isArray(props.successMessages) ? props.successMessages : [props.successMessages]\n  }\n  if (props.success) {\n    return [props.success]\n  }\n  return []\n})\n\nconst hasError = computed(() => computedErrorMessages.value.length > 0)\nconst hasSuccess = computed(() => computedSuccessMessages.value.length > 0 && validated.value)\n\n// Counter (legacy)\nconst computedCounter = computed(() => {\n  if (typeof props.counter === 'number') return props.counter\n  if (props.counter) return props.maxLength ?? 100\n  return null\n})\n\nconst currentLength = computed(() => String(internalValue.value ?? '').length)\n\n// Show count (Ant Design API)\nconst showCountEnabled = computed(() => {\n  return props.showCount !== undefined && props.showCount !== false\n})\n\nconst showCountConfig = computed<{ formatter?: (count: number, maxLength?: number) => string }>(() => {\n  if (typeof props.showCount === 'object') {\n    return props.showCount\n  }\n  return {}\n})\n\nconst countText = computed(() => {\n  const formatter = showCountConfig.value.formatter\n  if (formatter) {\n    return formatter(currentLength.value, props.maxLength)\n  }\n  if (props.maxLength !== undefined) {\n    return `${currentLength.value} / ${props.maxLength}`\n  }\n  return `${currentLength.value}`\n})\n\n// Allow clear\nconst showClear = computed(() => {\n  return props.allowClear && !props.disabled && !props.readonly && String(internalValue.value ?? '').length > 0\n})\n\n// Variant classes\nconst variantClasses = computed(() => {\n  const base = 'w-full transition-colors duration-200'\n\n  switch (props.variant) {\n    case 'outlined':\n      return cn(\n        base,\n        'border-2 rounded-lg',\n        focused.value ? 'border-primary ring-2 ring-primary/20' : 'border-input',\n        hasError.value && 'border-destructive focus:border-destructive focus:ring-destructive/20',\n      )\n    case 'filled':\n      return cn(\n        base,\n        'border-b-2 bg-muted/50 rounded-t-lg',\n        focused.value ? 'border-primary bg-muted' : 'border-transparent',\n        hasError.value && 'border-destructive',\n      )\n    case 'solo':\n      return cn(\n        base,\n        'rounded-lg shadow-sm',\n        focused.value ? 'shadow-md' : 'shadow-sm',\n        'bg-card border border-transparent',\n      )\n    case 'underlined':\n      return cn(\n        base,\n        'border-b-2 rounded-none border-x-0 border-t-0 px-0',\n        focused.value ? 'border-primary' : 'border-muted-foreground/30',\n        hasError.value && 'border-destructive',\n      )\n    case 'plain':\n      return cn(base, 'border-0 bg-transparent')\n    default:\n      return base\n  }\n})\n\n// Density classes\nconst densityClasses = computed(() => {\n  switch (props.density) {\n    case 'compact':\n      return 'text-sm min-h-[32px]'\n    case 'comfortable':\n      return 'text-base min-h-[40px]'\n    case 'default':\n    default:\n      return 'text-base min-h-[48px]'\n  }\n})\n\n// Resize classes\nconst resizeClasses = computed(() => {\n  if (props.noResize) return 'resize-none'\n  if (anyAutoResize.value) return 'resize-none'\n  return 'resize-y'\n})\n\n// Watch for programmatic value changes to trigger auto-resize\nwatch(internalValue, () => {\n  if (anyAutoResize.value) {\n    nextTick(() => autoResize())\n  }\n})\n\nonMounted(() => {\n  nextTick(() => {\n    measureHeights()\n    if (anyAutoResize.value) {\n      autoResize()\n    }\n  })\n})\n</script>\n\n<template>\n  <div :class=\"cn('relative space-y-2', props.class)\">\n    <!-- Label -->\n    <Label\n      v-if=\"label\"\n      :for=\"textareaId\"\n      :class=\"[\n        'text-foreground text-sm font-medium',\n        props.labelClass,\n        focused && 'text-primary',\n        hasError && 'text-destructive',\n      ]\"\n    >\n      {{ label }}\n      <span v-if=\"required\" class=\"text-destructive ml-0.5\">*</span>\n    </Label>\n\n    <!-- Control wrapper -->\n    <div\n      :class=\"\n        cn(\n          'relative flex items-center',\n          variantClasses,\n          densityClasses,\n          disabled && 'pointer-events-none opacity-50',\n          props.readonly && !disabled && 'cursor-default',\n          props.rounded !== 'none' && `rounded-${props.rounded}`,\n        )\n      \"\n      :style=\"props.bgColor ? { backgroundColor: props.bgColor } : {}\"\n    >\n      <!-- Prefix -->\n      <span\n        v-if=\"prefix\"\n        class=\"text-muted-foreground pointer-events-none absolute top-3 left-3 text-sm\"\n        :class=\"{ 'opacity-50': !persistentPrefix && !focused }\"\n      >\n        {{ prefix }}\n      </span>\n\n      <!-- Textarea -->\n      <textarea\n        :id=\"textareaId\"\n        :ref=\"\n          (el) => {\n            textareaRef = el as HTMLTextAreaElement\n          }\n        \"\n        :value=\"internalValue\"\n        :placeholder=\"placeholder\"\n        :disabled=\"disabled\"\n        :readonly=\"readonly\"\n        :required=\"required\"\n        :name=\"name\"\n        :autocomplete=\"autocomplete\"\n        :autofocus=\"autofocus\"\n        :spellcheck=\"spellcheck\"\n        :maxlength=\"maxLength\"\n        :rows=\"computedRows\"\n        :aria-describedby=\"descriptionId\"\n        :aria-invalid=\"hasError\"\n        :class=\"\n          cn(\n            'w-full flex-1 resize-y bg-transparent outline-none',\n            densityClasses,\n            resizeClasses,\n            prefix ? 'pl-16' : 'pl-3',\n            suffix ? 'pr-16' : showClear ? 'pr-10' : 'pr-3',\n            showCountEnabled && 'pb-6',\n            'py-2',\n            props.inputClass,\n          )\n        \"\n        @input=\"handleInput\"\n        @focus=\"handleFocus\"\n        @blur=\"handleBlur\"\n        @keydown=\"emits('keydown')\"\n        @keyup=\"emits('keyup')\"\n      />\n\n      <!-- Suffix -->\n      <span\n        v-if=\"suffix\"\n        class=\"text-muted-foreground pointer-events-none absolute top-3 right-3 text-sm\"\n        :class=\"{ 'opacity-50': !persistentSuffix && !focused }\"\n      >\n        {{ suffix }}\n      </span>\n\n      <!-- Clear button -->\n      <button\n        v-if=\"showClear\"\n        type=\"button\"\n        tabindex=\"-1\"\n        class=\"text-muted-foreground hover:text-foreground focus-visible:ring-ring absolute top-3 flex items-center justify-center rounded-sm transition-colors focus-visible:ring-2 focus-visible:outline-none\"\n        :class=\"suffix ? 'right-10' : 'right-3'\"\n        @click=\"handleClear\"\n      >\n        <X class=\"size-4\" aria-hidden=\"true\" />\n      </button>\n\n      <!-- Loading spinner -->\n      <div v-if=\"loading\" class=\"absolute top-3 right-3 flex items-center justify-center\">\n        <Loader class=\"text-muted-foreground size-4 animate-spin\" />\n      </div>\n\n      <!-- Success/Error indicators -->\n      <div\n        v-if=\"hasSuccess && !loading\"\n        class=\"absolute top-3 right-3 flex items-center justify-center text-[var(--success)]\"\n      >\n        <Check class=\"size-4\" aria-hidden=\"true\" />\n      </div>\n      <div v-if=\"hasError && !loading\" class=\"text-destructive absolute top-3 right-3 flex items-center justify-center\">\n        <AlertCircle class=\"size-4\" aria-hidden=\"true\" />\n      </div>\n\n      <!-- Show count -->\n      <div\n        v-if=\"showCountEnabled\"\n        class=\"text-muted-foreground pointer-events-none absolute right-3 bottom-1.5 text-xs\"\n        :class=\"{ 'text-destructive': props.maxLength !== undefined && currentLength > props.maxLength }\"\n      >\n        {{ countText }}\n      </div>\n    </div>\n\n    <!-- Messages (hint, error, success) -->\n    <div class=\"mt-1.5\">\n      <!-- Hint -->\n      <p\n        v-if=\"hint && (!hasError || persistentHint) && !focused\"\n        :class=\"cn('text-muted-foreground text-sm', props.hintClass)\"\n      >\n        {{ hint }}\n      </p>\n\n      <!-- Error messages -->\n      <p\n        v-for=\"(msg, i) in computedErrorMessages\"\n        :key=\"`error-${i}`\"\n        class=\"text-destructive flex items-center gap-1 text-sm\"\n      >\n        <AlertCircle class=\"size-3 shrink-0\" aria-hidden=\"true\" />\n        {{ msg }}\n      </p>\n\n      <!-- Success messages -->\n      <p\n        v-for=\"(msg, i) in computedSuccessMessages\"\n        :key=\"`success-${i}`\"\n        class=\"flex items-center gap-1 text-sm text-[var(--success)]\"\n      >\n        <Check class=\"size-3 shrink-0\" />\n        {{ msg }}\n      </p>\n\n      <!-- Counter (legacy) -->\n      <div\n        v-if=\"computedCounter !== null\"\n        class=\"text-muted-foreground mt-1 text-right text-xs\"\n        :class=\"{ 'text-destructive': currentLength > computedCounter }\"\n      >\n        {{ currentLength }} / {{ computedCounter }}\n      </div>\n    </div>\n\n    <slot />\n  </div>\n</template>\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/textarea/Textarea.vue"
    },
    {
      "path": "packages/registry-vue/components/textarea/index.ts",
      "content": "export { default as Textarea } from './Textarea.vue'\n",
      "type": "registry:ui",
      "target": "~/app/components/ui/textarea/index.ts"
    }
  ],
  "dependencies": [
    "@vueuse/core",
    "lucide-vue-next",
    "reka-ui"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/vue/label.json"
  ],
  "description": "Multi-line text input. Auto-resize variant, character counter, and the same ring/border treatment as the rest of the form primitives.",
  "categories": [
    "form"
  ]
}