{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "textarea",
  "title": "Textarea",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-svelte/components/textarea/Textarea.svelte",
      "content": "<script lang=\"ts\" module>\n  import type { Snippet } from 'svelte'\n\n  import type { HTMLTextareaAttributes } from 'svelte/elements'\n\n  export interface TextareaProps {\n    // Core\n    value?: 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}).\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: unknown) => 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?: string\n    inputClass?: string\n    labelClass?: string\n    hintClass?: string\n    bgColor?: string\n    flat?: boolean\n    bordered?: boolean\n\n    // Browser\n    spellcheck?: boolean\n    autocomplete?: HTMLTextareaAttributes['autocomplete']\n\n    // Direction\n    direction?: 'ltr' | 'rtl'\n\n    // Events (Svelte callback props mirroring the Vue emits)\n    onclear?: () => void\n    onfocus?: (event?: FocusEvent) => void\n    onblur?: (event?: FocusEvent) => void\n    onkeydown?: (event: KeyboardEvent) => void\n    onkeyup?: (event?: KeyboardEvent) => void\n\n    children?: Snippet\n    ref?: HTMLTextAreaElement | null\n  }\n\n  let textareaCounter = 0\n</script>\n\n<script lang=\"ts\">\n  import { tick } from 'svelte'\n  import { cn } from '$lib/utils'\n  import { Check, CircleAlert, LoaderCircle, X } from '@lucide/svelte'\n\n  let {\n    defaultValue = '',\n    value = $bindable(defaultValue),\n    label,\n    placeholder,\n    hint,\n    error,\n    success,\n    disabled = false,\n    readonly = false,\n    required = false,\n    autofocus,\n    name,\n    id,\n    variant = 'outlined',\n    density = 'default',\n    rounded = 'none',\n    autoSize,\n    autoGrow = false,\n    noResize = false,\n    autoResize: autoResizeProp = false,\n    rows = 3,\n    rowHeight = 24,\n    prefix,\n    suffix,\n    counter,\n    showCount,\n    maxLength,\n    allowClear = false,\n    rules,\n    errorMessages,\n    successMessages,\n    validateOn,\n    loading = false,\n    persistentHint = false,\n    persistentError: _persistentError = false,\n    persistentPlaceholder: _persistentPlaceholder = false,\n    persistentPrefix = false,\n    persistentSuffix = false,\n    class: className,\n    inputClass,\n    labelClass,\n    hintClass,\n    bgColor,\n    flat: _flat = false,\n    bordered: _bordered = true,\n    spellcheck,\n    autocomplete,\n    direction = 'ltr',\n    onclear,\n    onfocus,\n    onblur,\n    onkeydown,\n    onkeyup,\n    children,\n    ref = $bindable(null),\n  }: TextareaProps = $props()\n\n  // NOTE: `color` and `messages` are accepted for Vue API parity but intentionally\n  // unwired — the Svelte port styles validation state through variant classes.\n\n  const autoId = `textarea-${++textareaCounter}`\n  const textareaId = $derived(id ?? autoId)\n  const descriptionId = $derived(`${textareaId}-description`)\n\n  let focused = $state(false)\n  let internalErrorMessages = $state<string[]>([])\n  let textareaEl: HTMLTextAreaElement | null = $state(null)\n\n  $effect(() => {\n    ref = textareaEl\n  })\n\n  // Auto size\n  const autoSizeEnabled = $derived(autoSize !== undefined)\n  const anyAutoResize = $derived(autoSizeEnabled || autoResizeProp || autoGrow)\n\n  const autoSizeConfig = $derived<{ minRows?: number; maxRows?: number }>(\n    typeof autoSize === 'object' ? autoSize : {},\n  )\n\n  let minHeightPx = $state(0)\n  let maxHeightPx = $state(Infinity)\n\n  function measureHeights() {\n    if (!textareaEl) return\n    if (!autoSizeEnabled) return\n    const el = textareaEl\n    const originalValue = el.value\n    const originalRows = el.rows\n    const originalOverflow = el.style.overflowY\n    el.value = ''\n    el.style.overflowY = 'hidden'\n    const { minRows, maxRows } = autoSizeConfig\n    if (minRows) {\n      el.rows = minRows\n      minHeightPx = el.scrollHeight\n    } else {\n      minHeightPx = 0\n    }\n    if (maxRows) {\n      el.rows = maxRows\n      maxHeightPx = el.scrollHeight\n    } else {\n      maxHeightPx = Infinity\n    }\n    el.value = originalValue\n    el.rows = originalRows\n    el.style.overflowY = originalOverflow\n    doAutoResize()\n  }\n\n  $effect(() => {\n    // Re-measure when the config object identity changes.\n    void autoSizeConfig\n    tick().then(() => measureHeights())\n  })\n\n  // Auto grow functionality (legacy)\n  const rowsNum = $derived(Number(rows) || 3)\n\n  const computedRows = $derived.by(() => {\n    if (autoSizeEnabled || autoResizeProp) return rowsNum\n    if (!autoGrow) return rowsNum\n    if (!textareaEl) return rowsNum\n    const lineHeight = rowHeight\n    const computedHeight = textareaEl.scrollHeight\n    const newRows = Math.ceil((computedHeight - lineHeight) / lineHeight) + 1\n    return Math.max(rowsNum, newRows)\n  })\n\n  // Validation\n  function validate() {\n    if (!rules || rules.length === 0) return true\n    internalErrorMessages = []\n    for (const rule of rules) {\n      const result = rule(value)\n      if (result !== true) {\n        internalErrorMessages.push(result as string)\n      }\n    }\n    return internalErrorMessages.length === 0\n  }\n\n  function handleInput(e: Event) {\n    const target = e.target as HTMLTextAreaElement\n    value = target.value\n    if (anyAutoResize) {\n      doAutoResize()\n    }\n    if (validateOn === 'input' || validateOn === 'inputlazy') {\n      // nextTick so value is settled before rules run\n      tick().then(() => validate())\n    }\n  }\n\n  function doAutoResize() {\n    if (!textareaEl) return\n    if (!anyAutoResize) return\n    const el = textareaEl\n    el.style.height = 'auto'\n    let newHeight = el.scrollHeight\n    if (minHeightPx && newHeight < minHeightPx) {\n      newHeight = minHeightPx\n    }\n    if (newHeight > maxHeightPx) {\n      newHeight = maxHeightPx\n      el.style.overflowY = 'auto'\n    } else {\n      el.style.overflowY = 'hidden'\n    }\n    el.style.height = `${newHeight}px`\n  }\n\n  // Handle clear\n  function handleClear() {\n    value = ''\n    onclear?.()\n    tick().then(() => {\n      doAutoResize()\n      textareaEl?.focus()\n    })\n  }\n\n  // Handle focus/blur\n  function handleFocus(event: FocusEvent) {\n    focused = true\n    onfocus?.(event)\n  }\n\n  function handleBlur(event: FocusEvent) {\n    focused = false\n    if (validateOn === 'blur' || validateOn === 'blurlazy') {\n      validate()\n    }\n    onblur?.(event)\n  }\n\n  // Compute error/success messages\n  const computedErrorMessages = $derived.by((): string[] => {\n    if (errorMessages) {\n      return Array.isArray(errorMessages) ? errorMessages : [errorMessages]\n    }\n    if (error) {\n      return [error]\n    }\n    return internalErrorMessages\n  })\n\n  const computedSuccessMessages = $derived.by((): string[] => {\n    if (successMessages) {\n      return Array.isArray(successMessages) ? successMessages : [successMessages]\n    }\n    if (success) {\n      return [success]\n    }\n    return []\n  })\n\n  const hasError = $derived(computedErrorMessages.length > 0)\n  const hasSuccess = $derived(computedSuccessMessages.length > 0)\n\n  // Counter (legacy)\n  const computedCounter = $derived.by((): number | null => {\n    if (typeof counter === 'number') return counter\n    if (counter) return maxLength ?? 100\n    return null\n  })\n\n  const currentLength = $derived(String(value ?? '').length)\n\n  // Show count (Ant Design API)\n  const showCountEnabled = $derived(showCount !== undefined && showCount !== false)\n\n  const showCountConfig = $derived<{ formatter?: (count: number, maxLength?: number) => string }>(\n    typeof showCount === 'object' ? showCount : {},\n  )\n\n  const countText = $derived.by(() => {\n    const formatter = showCountConfig.formatter\n    if (formatter) {\n      return formatter(currentLength, maxLength)\n    }\n    if (maxLength !== undefined) {\n      return `${currentLength} / ${maxLength}`\n    }\n    return `${currentLength}`\n  })\n\n  // Allow clear\n  const showClear = $derived(\n    Boolean(allowClear && !disabled && !readonly && String(value ?? '').length > 0),\n  )\n\n  // Variant classes\n  const variantClasses = $derived.by(() => {\n    const base = 'w-full transition-colors duration-200'\n    switch (variant) {\n      case 'outlined':\n        return cn(\n          base,\n          'border-2 rounded-lg',\n          focused ? 'border-primary ring-2 ring-primary/20' : 'border-input',\n          hasError && '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 ? 'border-primary bg-muted' : 'border-transparent',\n          hasError && 'border-destructive',\n        )\n      case 'solo':\n        return cn(\n          base,\n          'rounded-lg shadow-sm',\n          focused ? '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 ? 'border-primary' : 'border-muted-foreground/30',\n          hasError && '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\n  const densityClasses = $derived.by(() => {\n    switch (density) {\n      case 'compact':\n        return 'text-sm min-h-8'\n      case 'comfortable':\n        return 'text-base min-h-10'\n      case 'default':\n      default:\n        return 'text-base min-h-12'\n    }\n  })\n\n  // Resize classes\n  const resizeClasses = $derived.by(() => {\n    if (noResize) return 'resize-none'\n    if (anyAutoResize) return 'resize-none'\n    return 'resize-y'\n  })\n\n  // Watch for programmatic value changes to trigger auto-resize (also runs on mount).\n  $effect(() => {\n    void value\n    if (anyAutoResize) {\n      tick().then(() => doAutoResize())\n    }\n  })\n</script>\n\n<div data-uipkge=\"\" data-slot=\"textarea\" dir={direction} class={cn('relative space-y-2', className)}>\n  <!-- Label -->\n  {#if label}\n    <label\n      for={textareaId}\n      data-uipkge=\"\"\n      data-slot=\"label\"\n      class={cn(\n        'text-foreground text-sm font-medium',\n        labelClass,\n        focused && 'text-primary',\n        hasError && 'text-destructive',\n      )}\n    >\n      {label}\n      {#if required}\n        <span class=\"text-destructive ml-0.5\">*</span>\n      {/if}\n    </label>\n  {/if}\n\n  <!-- Control wrapper -->\n  <div\n    class={cn(\n      'relative flex items-center',\n      variantClasses,\n      densityClasses,\n      disabled && 'pointer-events-none opacity-50',\n      readonly && !disabled && 'cursor-default',\n      rounded !== 'none' && `rounded-${rounded}`,\n    )}\n    style={bgColor ? `background-color: ${bgColor}` : undefined}\n  >\n    <!-- Prefix -->\n    {#if prefix}\n      <span\n        class={cn('text-muted-foreground pointer-events-none absolute top-3 left-3 text-sm', {\n          'opacity-50': !persistentPrefix && !focused,\n        })}\n      >\n        {prefix}\n      </span>\n    {/if}\n\n    <!-- Textarea -->\n    <!-- svelte-ignore a11y_autofocus: autofocus is an explicit opt-in consumer prop -->\n    <textarea\n      id={textareaId}\n      bind:this={textareaEl}\n      value={value ?? ''}\n      {placeholder}\n      {disabled}\n      {readonly}\n      {required}\n      {name}\n      {autocomplete}\n      {autofocus}\n      {spellcheck}\n      maxlength={maxLength}\n      rows={computedRows}\n      aria-describedby={hasError || hasSuccess || hint ? descriptionId : undefined}\n      aria-invalid={hasError || undefined}\n      class={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        inputClass,\n      )}\n      oninput={handleInput}\n      onfocus={handleFocus}\n      onblur={handleBlur}\n      onkeydown={(e) => onkeydown?.(e)}\n      onkeyup={(e) => onkeyup?.(e)}\n    ></textarea>\n\n    <!-- Suffix -->\n    {#if suffix}\n      <span\n        class={cn('text-muted-foreground pointer-events-none absolute top-3 right-3 text-sm', {\n          'opacity-50': !persistentSuffix && !focused,\n        })}\n      >\n        {suffix}\n      </span>\n    {/if}\n\n    <!-- Clear button -->\n    {#if showClear}\n      <button\n        type=\"button\"\n        tabindex=\"-1\"\n        aria-label=\"Clear\"\n        class={cn(\n          '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          suffix ? 'right-10' : 'right-3',\n        )}\n        onclick={handleClear}\n      >\n        <X class=\"size-4\" aria-hidden=\"true\" />\n      </button>\n    {/if}\n\n    <!-- Loading spinner -->\n    {#if loading}\n      <div class=\"absolute top-3 right-3 flex items-center justify-center\">\n        <LoaderCircle class=\"text-muted-foreground size-4 animate-spin\" />\n      </div>\n    {/if}\n\n    <!-- Success/Error indicators -->\n    {#if hasSuccess && !loading}\n      <div class=\"text-success absolute top-3 right-3 flex items-center justify-center\">\n        <Check class=\"size-4\" aria-hidden=\"true\" />\n      </div>\n    {/if}\n    {#if hasError && !loading}\n      <div class=\"text-destructive absolute top-3 right-3 flex items-center justify-center\">\n        <CircleAlert class=\"size-4\" aria-hidden=\"true\" />\n      </div>\n    {/if}\n\n    <!-- Show count -->\n    {#if showCountEnabled}\n      <div\n        class={cn('text-muted-foreground pointer-events-none absolute right-3 bottom-1.5 text-xs', {\n          'text-destructive': maxLength !== undefined && currentLength > maxLength,\n        })}\n      >\n        {countText}\n      </div>\n    {/if}\n  </div>\n\n  <!-- Messages (hint, error, success) -->\n  <div id={descriptionId} class=\"mt-1.5\">\n    <!-- Hint -->\n    {#if hint && (!hasError || persistentHint) && !focused}\n      <p class={cn('text-muted-foreground text-sm', hintClass)}>\n        {hint}\n      </p>\n    {/if}\n\n    <!-- Error messages -->\n    {#each computedErrorMessages as msg, i (`error-${i}`)}\n      <p class=\"text-destructive flex items-center gap-1 text-sm\" role=\"alert\">\n        <CircleAlert class=\"size-3 shrink-0\" aria-hidden=\"true\" />\n        {msg}\n      </p>\n    {/each}\n\n    <!-- Success messages -->\n    {#each computedSuccessMessages as msg, i (`success-${i}`)}\n      <p class=\"text-success flex items-center gap-1 text-sm\">\n        <Check class=\"size-3 shrink-0\" aria-hidden=\"true\" />\n        {msg}\n      </p>\n    {/each}\n\n    <!-- Counter (legacy) -->\n    {#if computedCounter !== null}\n      <div\n        class={cn('text-muted-foreground mt-1 text-right text-xs', {\n          'text-destructive': currentLength > computedCounter,\n        })}\n      >\n        {currentLength} / {computedCounter}\n      </div>\n    {/if}\n  </div>\n\n  {@render children?.()}\n</div>\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/textarea/Textarea.svelte"
    },
    {
      "path": "packages/registry-svelte/components/textarea/index.ts",
      "content": "export { default as Textarea, type TextareaProps } from './Textarea.svelte'\n",
      "type": "registry:ui",
      "target": "~/src/lib/components/ui/textarea/index.ts"
    }
  ],
  "dependencies": [
    "@lucide/svelte"
  ],
  "devDependencies": [],
  "registryDependencies": [],
  "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"
  ]
}