{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "textarea",
  "title": "Textarea",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-react/components/textarea/Textarea.tsx",
      "content": "'use client'\n\nimport * as React from 'react'\nimport { Loader, Check, AlertCircle, X } from 'lucide-react'\nimport { cn } from '@/lib/utils'\nimport { Label } from '@/components/ui/label'\n\nexport interface TextareaProps {\n  // Core\n  value?: string | number\n  defaultValue?: string | number\n  onValueChange?: (value: string) => void\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: 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  className?: string\n  inputClassName?: string\n  labelClassName?: string\n  hintClassName?: string\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  // Events\n  onClear?: () => void\n  onFocus?: () => void\n  onBlur?: () => void\n  onKeyDown?: () => void\n  onKeyUp?: () => void\n\n  children?: React.ReactNode\n}\n\nconst Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>((props, forwardedRef) => {\n  const {\n    value,\n    defaultValue,\n    onValueChange,\n    label,\n    placeholder,\n    hint,\n    error,\n    success,\n    disabled,\n    readOnly,\n    required,\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 = false,\n    rows = 3,\n    rowHeight = 24,\n    prefix,\n    suffix,\n    counter,\n    showCount,\n    maxLength,\n    allowClear,\n    rules,\n    errorMessages,\n    successMessages,\n    validateOn,\n    loading,\n    persistentHint = false,\n    persistentPrefix,\n    persistentSuffix,\n    className,\n    inputClassName,\n    labelClassName,\n    hintClassName,\n    bgColor,\n    spellCheck,\n    autoComplete,\n    onClear,\n    onFocus,\n    onBlur,\n    onKeyDown,\n    onKeyUp,\n    children,\n  } = props\n\n  const reactId = React.useId()\n  const textareaId = id ?? `textarea-${reactId}`\n  const descriptionId = `${textareaId}-description`\n\n  const textareaRef = React.useRef<HTMLTextAreaElement | null>(null)\n  const setRefs = React.useCallback(\n    (node: HTMLTextAreaElement | null) => {\n      textareaRef.current = node\n      if (typeof forwardedRef === 'function') forwardedRef(node)\n      else if (forwardedRef) (forwardedRef as React.MutableRefObject<HTMLTextAreaElement | null>).current = node\n    },\n    [forwardedRef],\n  )\n\n  // Internal state\n  const isControlled = value !== undefined\n  const [internal, setInternal] = React.useState<string | number>(defaultValue ?? '')\n  const internalValue = isControlled ? value! : internal\n\n  const [focused, setFocused] = React.useState(false)\n  const [internalErrorMessages, setInternalErrorMessages] = React.useState<string[]>([])\n  const [validated] = React.useState(false)\n\n  // Auto size\n  const autoSizeEnabled = autoSize !== undefined\n  const anyAutoResize = autoSizeEnabled || autoResize || autoGrow\n\n  const autoSizeConfig = React.useMemo<{ minRows?: number; maxRows?: number }>(() => {\n    if (typeof autoSize === 'object') return autoSize\n    return {}\n  }, [autoSize])\n\n  const minHeightPx = React.useRef(0)\n  const maxHeightPx = React.useRef(Infinity)\n\n  const autoResizeFn = React.useCallback(() => {\n    if (!textareaRef.current) return\n    if (!anyAutoResize) return\n\n    const el = textareaRef.current\n\n    el.style.height = 'auto'\n    let newHeight = el.scrollHeight\n\n    if (minHeightPx.current && newHeight < minHeightPx.current) {\n      newHeight = minHeightPx.current\n    }\n\n    if (newHeight > maxHeightPx.current) {\n      newHeight = maxHeightPx.current\n      el.style.overflowY = 'auto'\n    } else {\n      el.style.overflowY = 'hidden'\n    }\n\n    el.style.height = `${newHeight}px`\n  }, [anyAutoResize])\n\n  const measureHeights = React.useCallback(() => {\n    if (!textareaRef.current) return\n    if (!autoSizeEnabled) return\n\n    const el = textareaRef.current\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\n\n    if (minRows) {\n      el.rows = minRows\n      minHeightPx.current = el.scrollHeight\n    } else {\n      minHeightPx.current = 0\n    }\n\n    if (maxRows) {\n      el.rows = maxRows\n      maxHeightPx.current = el.scrollHeight\n    } else {\n      maxHeightPx.current = Infinity\n    }\n\n    el.value = originalValue\n    el.rows = originalRows\n    el.style.overflowY = originalOverflow\n\n    autoResizeFn()\n  }, [autoSizeEnabled, autoSizeConfig, autoResizeFn])\n\n  // Auto grow functionality (legacy)\n  const rowsNum = Number(rows) || 3\n\n  const computedRows = React.useMemo(() => {\n    if (autoSizeEnabled || autoResize) return rowsNum\n    if (!autoGrow) return rowsNum\n    if (!textareaRef.current) return rowsNum\n\n    const lineHeight = rowHeight\n    const computedHeight = textareaRef.current.scrollHeight\n    const newRows = Math.ceil((computedHeight - lineHeight) / lineHeight) + 1\n    return Math.max(rowsNum, newRows)\n  }, [autoSizeEnabled, autoResize, autoGrow, rowsNum, rowHeight, internalValue])\n\n  // Validation\n  const validate = React.useCallback(() => {\n    if (!rules || rules.length === 0) return true\n    const next: string[] = []\n    for (const rule of rules) {\n      const result = rule(internalValue)\n      if (result !== true) {\n        next.push(result as string)\n      }\n    }\n    setInternalErrorMessages(next)\n    return next.length === 0\n  }, [rules, internalValue])\n\n  // Handle input\n  const setValue = (next: string) => {\n    if (!isControlled) setInternal(next)\n    onValueChange?.(next)\n  }\n\n  const handleInput = (e: React.ChangeEvent<HTMLTextAreaElement>) => {\n    setValue(e.target.value)\n    if (anyAutoResize) {\n      autoResizeFn()\n    }\n  }\n\n  // Handle clear\n  const handleClear = () => {\n    setValue('')\n    onClear?.()\n    requestAnimationFrame(() => {\n      autoResizeFn()\n      textareaRef.current?.focus()\n    })\n  }\n\n  // Handle focus/blur\n  const handleFocus = () => {\n    setFocused(true)\n    onFocus?.()\n  }\n\n  const handleBlur = () => {\n    setFocused(false)\n    if (validateOn === 'blur' || validateOn === 'blurlazy') {\n      validate()\n    }\n    onBlur?.()\n  }\n\n  // Compute error/success messages\n  const computedErrorMessages = React.useMemo(() => {\n    if (errorMessages) {\n      return Array.isArray(errorMessages) ? errorMessages : [errorMessages]\n    }\n    if (error) {\n      return [error]\n    }\n    return internalErrorMessages\n  }, [errorMessages, error, internalErrorMessages])\n\n  const computedSuccessMessages = React.useMemo(() => {\n    if (successMessages) {\n      return Array.isArray(successMessages) ? successMessages : [successMessages]\n    }\n    if (success) {\n      return [success]\n    }\n    return []\n  }, [successMessages, success])\n\n  const hasError = computedErrorMessages.length > 0\n  const hasSuccess = computedSuccessMessages.length > 0 && validated\n\n  // Counter (legacy)\n  const computedCounter = React.useMemo(() => {\n    if (typeof counter === 'number') return counter\n    if (counter) return maxLength ?? 100\n    return null\n  }, [counter, maxLength])\n\n  const currentLength = String(internalValue ?? '').length\n\n  // Show count (Ant Design API)\n  const showCountEnabled = showCount !== undefined && showCount !== false\n\n  const showCountConfig = React.useMemo<{ formatter?: (count: number, maxLength?: number) => string }>(() => {\n    if (typeof showCount === 'object') return showCount\n    return {}\n  }, [showCount])\n\n  const countText = React.useMemo(() => {\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  }, [showCountConfig, currentLength, maxLength])\n\n  // Allow clear\n  const showClear = allowClear && !disabled && !readOnly && String(internalValue ?? '').length > 0\n\n  // Variant classes\n  const variantClasses = React.useMemo(() => {\n    const base = 'w-full transition-colors duration-200'\n\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  }, [variant, focused, hasError])\n\n  // Density classes\n  const densityClasses = React.useMemo(() => {\n    switch (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  }, [density])\n\n  // Resize classes\n  const resizeClasses = React.useMemo(() => {\n    if (noResize) return 'resize-none'\n    if (anyAutoResize) return 'resize-none'\n    return 'resize-y'\n  }, [noResize, anyAutoResize])\n\n  // Watch for programmatic value changes to trigger auto-resize\n  React.useEffect(() => {\n    if (anyAutoResize) {\n      requestAnimationFrame(() => autoResizeFn())\n    }\n  }, [internalValue, anyAutoResize, autoResizeFn])\n\n  React.useEffect(() => {\n    requestAnimationFrame(() => {\n      measureHeights()\n      if (anyAutoResize) {\n        autoResizeFn()\n      }\n    })\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [])\n\n  return (\n    <div className={cn('relative space-y-2', className)}>\n      {/* Label */}\n      {label && (\n        <Label\n          htmlFor={textareaId}\n          className={cn(\n            'text-foreground text-sm font-medium',\n            labelClassName,\n            focused && 'text-primary',\n            hasError && 'text-destructive',\n          )}\n        >\n          {label}\n          {required && <span className=\"text-destructive ml-0.5\">*</span>}\n        </Label>\n      )}\n\n      {/* Control wrapper */}\n      <div\n        className={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 ? { backgroundColor: bgColor } : undefined}\n      >\n        {/* Prefix */}\n        {prefix && (\n          <span\n            className={cn(\n              'text-muted-foreground pointer-events-none absolute top-3 left-3 text-sm',\n              !persistentPrefix && !focused && 'opacity-50',\n            )}\n          >\n            {prefix}\n          </span>\n        )}\n\n        {/* Textarea */}\n        <textarea\n          id={textareaId}\n          ref={setRefs}\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          className={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            inputClassName,\n          )}\n          onChange={handleInput}\n          onFocus={handleFocus}\n          onBlur={handleBlur}\n          onKeyDown={() => onKeyDown?.()}\n          onKeyUp={() => onKeyUp?.()}\n        />\n\n        {/* Suffix */}\n        {suffix && (\n          <span\n            className={cn(\n              'text-muted-foreground pointer-events-none absolute top-3 right-3 text-sm',\n              !persistentSuffix && !focused && 'opacity-50',\n            )}\n          >\n            {suffix}\n          </span>\n        )}\n\n        {/* Clear button */}\n        {showClear && (\n          <button\n            type=\"button\"\n            tabIndex={-1}\n            className={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 className=\"size-4\" aria-hidden=\"true\" />\n          </button>\n        )}\n\n        {/* Loading spinner */}\n        {loading && (\n          <div className=\"absolute top-3 right-3 flex items-center justify-center\">\n            <Loader className=\"text-muted-foreground size-4 animate-spin\" />\n          </div>\n        )}\n\n        {/* Success/Error indicators */}\n        {hasSuccess && !loading && (\n          <div className=\"absolute top-3 right-3 flex items-center justify-center text-[var(--success)]\">\n            <Check className=\"size-4\" aria-hidden=\"true\" />\n          </div>\n        )}\n        {hasError && !loading && (\n          <div className=\"text-destructive absolute top-3 right-3 flex items-center justify-center\">\n            <AlertCircle className=\"size-4\" aria-hidden=\"true\" />\n          </div>\n        )}\n\n        {/* Show count */}\n        {showCountEnabled && (\n          <div\n            className={cn(\n              'text-muted-foreground pointer-events-none absolute right-3 bottom-1.5 text-xs',\n              maxLength !== undefined && currentLength > maxLength && 'text-destructive',\n            )}\n          >\n            {countText}\n          </div>\n        )}\n      </div>\n\n      {/* Messages (hint, error, success) */}\n      <div className=\"mt-1.5\">\n        {/* Hint */}\n        {hint && (!hasError || persistentHint) && !focused && (\n          <p className={cn('text-muted-foreground text-sm', hintClassName)}>{hint}</p>\n        )}\n\n        {/* Error messages */}\n        {computedErrorMessages.map((msg, i) => (\n          <p key={`error-${i}`} className=\"text-destructive flex items-center gap-1 text-sm\">\n            <AlertCircle className=\"size-3 shrink-0\" aria-hidden=\"true\" />\n            {msg}\n          </p>\n        ))}\n\n        {/* Success messages */}\n        {computedSuccessMessages.map((msg, i) => (\n          <p key={`success-${i}`} className=\"flex items-center gap-1 text-sm text-[var(--success)]\">\n            <Check className=\"size-3 shrink-0\" />\n            {msg}\n          </p>\n        ))}\n\n        {/* Counter (legacy) */}\n        {computedCounter !== null && (\n          <div\n            className={cn(\n              'text-muted-foreground mt-1 text-right text-xs',\n              currentLength > computedCounter && 'text-destructive',\n            )}\n          >\n            {currentLength} / {computedCounter}\n          </div>\n        )}\n      </div>\n\n      {children}\n    </div>\n  )\n})\nTextarea.displayName = 'Textarea'\n\nexport { Textarea }\n",
      "type": "registry:ui",
      "target": "~/components/ui/textarea/Textarea.tsx"
    },
    {
      "path": "packages/registry-react/components/textarea/index.ts",
      "content": "export { Textarea, type TextareaProps } from './Textarea'\n",
      "type": "registry:ui",
      "target": "~/components/ui/textarea/index.ts"
    }
  ],
  "dependencies": [
    "lucide-react"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/react/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"
  ]
}