{
  "schemaVersion": 1,
  "target": "react",
  "name": "textarea",
  "version": "0.1.0",
  "category": "core",
  "title": "Textarea",
  "description": "A multi-line text field with a label, helper text, errors, an optional character counter, and optional auto-grow.",
  "tags": [
    "form",
    "field",
    "text",
    "multiline",
    "counter"
  ],
  "requires": {
    "projectTypes": [
      "next",
      "vite-react"
    ],
    "styling": "tailwind-v4"
  },
  "npmDependencies": {},
  "registryDependencies": [],
  "docsPath": "/components/textarea",
  "props": [
    {
      "name": "label",
      "type": "ReactNode",
      "description": "Visible label, linked to the textarea. Use aria-label when the label is shown elsewhere."
    },
    {
      "name": "description",
      "type": "ReactNode",
      "description": "Helper text below the field, linked with aria-describedby."
    },
    {
      "name": "error",
      "type": "ReactNode",
      "description": "Error message. Sets aria-invalid and replaces the description."
    },
    {
      "name": "maxLength",
      "type": "number",
      "description": "Native character limit. Also turns on the character counter."
    },
    {
      "name": "showCount",
      "type": "boolean",
      "default": "true",
      "description": "Shows the counter when maxLength is set. Set to false to keep the limit without a counter."
    },
    {
      "name": "autoGrow",
      "type": "boolean",
      "default": "false",
      "description": "Grows with the content (CSS field-sizing: content) between 5rem and 16rem, then scrolls. Browsers without field-sizing fall back to rows."
    },
    {
      "name": "rows",
      "type": "number",
      "default": "3",
      "description": "Initial height in lines when autoGrow is off."
    },
    {
      "name": "containerClassName",
      "type": "string",
      "description": "Classes for the wrapper. className styles the textarea itself."
    },
    {
      "name": "...props",
      "type": "ComponentProps<\"textarea\">",
      "description": "Any native textarea attribute, including value, defaultValue, onChange, and disabled."
    }
  ],
  "accessibility": [
    "The label is a native <label> bound to the textarea with an id generated by useId.",
    "Helper text, errors, and the counter are linked with aria-describedby; errors also set aria-invalid.",
    "The visible counter reads as \"12 of 160 characters used\" to screen readers when the field is focused.",
    "A polite live region announces the characters left only near the limit and when it is reached, so typing is not interrupted on every keystroke.",
    "At the limit the counter changes weight as well as color, and the numbers themselves show the limit.",
    "Focus shows a border and ring; transitions are disabled with reduced motion."
  ],
  "states": [
    "default",
    "invalid",
    "disabled",
    "at-limit"
  ],
  "files": [
    {
      "path": "ui/textarea.tsx",
      "content": "\"use client\";\n\nimport { type ChangeEvent, type ComponentProps, type ReactNode, useId, useState } from \"react\";\n\nfunction cx(...classes: (string | false | null | undefined)[]) {\n  return classes.filter(Boolean).join(\" \");\n}\n\nexport interface TextareaProps extends ComponentProps<\"textarea\"> {\n  /** Visible label. Pass `aria-label` instead when the label is shown elsewhere. */\n  label?: ReactNode;\n  /** Helper text below the field. */\n  description?: ReactNode;\n  /** Error message. Marks the field invalid and replaces the description. */\n  error?: ReactNode;\n  /** Grows with its content between a minimum and maximum height, then scrolls. */\n  autoGrow?: boolean;\n  /** Shows a character counter when `maxLength` is set. */\n  showCount?: boolean;\n  /** Classes for the wrapping element. `className` styles the textarea itself. */\n  containerClassName?: string;\n}\n\nexport function Textarea({\n  label,\n  description,\n  error,\n  autoGrow = false,\n  showCount = true,\n  maxLength,\n  value,\n  defaultValue,\n  onChange,\n  id,\n  rows = 3,\n  className,\n  containerClassName,\n  disabled,\n  \"aria-describedby\": describedBy,\n  ...props\n}: TextareaProps) {\n  const generatedId = useId();\n  const textareaId = id ?? generatedId;\n  const messageId = `${textareaId}-message`;\n  const counterId = `${textareaId}-counter`;\n  const invalid = error != null && error !== false;\n  const message = invalid ? error : description;\n\n  const [uncontrolledLength, setUncontrolledLength] = useState(() => String(defaultValue ?? \"\").length);\n  const length = value !== undefined ? String(value ?? \"\").length : uncontrolledLength;\n  const counting = showCount && maxLength !== undefined && maxLength > 0;\n  const remaining = counting ? Math.max(maxLength - length, 0) : 0;\n  // Announce only near the limit so screen readers are not interrupted on every keystroke.\n  const warnAt = counting ? Math.max(10, Math.ceil(maxLength * 0.1)) : 0;\n  const announcement = !counting\n    ? \"\"\n    : remaining === 0\n      ? \"Character limit reached.\"\n      : remaining <= warnAt\n        ? `${remaining} ${remaining === 1 ? \"character\" : \"characters\"} left.`\n        : \"\";\n\n  function handleChange(event: ChangeEvent<HTMLTextAreaElement>) {\n    if (value === undefined) setUncontrolledLength(event.target.value.length);\n    onChange?.(event);\n  }\n\n  return (\n    <div className={cx(\"grid gap-1.5\", containerClassName)}>\n      {label ? (\n        <label htmlFor={textareaId} className=\"text-sm font-medium text-zinc-900 dark:text-zinc-100\">\n          {label}\n        </label>\n      ) : null}\n      <textarea\n        id={textareaId}\n        rows={rows}\n        disabled={disabled}\n        maxLength={maxLength}\n        value={value}\n        defaultValue={defaultValue}\n        onChange={handleChange}\n        aria-invalid={invalid || undefined}\n        aria-describedby={[message ? messageId : null, counting ? counterId : null, describedBy].filter(Boolean).join(\" \") || undefined}\n        className={cx(\n          \"block w-full rounded-lg border bg-white px-3 py-2 text-sm text-zinc-900 shadow-sm placeholder:text-zinc-400\",\n          \"transition-[border-color,box-shadow] duration-150 motion-reduce:transition-none\",\n          \"focus-visible:border-sky-500 focus-visible:ring-3 focus-visible:ring-sky-500/20 focus-visible:outline-none\",\n          \"disabled:cursor-not-allowed disabled:bg-zinc-50 disabled:opacity-60\",\n          \"dark:bg-zinc-950 dark:text-zinc-100 dark:placeholder:text-zinc-500 dark:disabled:bg-zinc-900\",\n          autoGrow ? \"field-sizing-content min-h-20 max-h-64 resize-none overflow-y-auto\" : \"resize-y\",\n          invalid\n            ? \"border-red-400 focus-visible:border-red-500 focus-visible:ring-red-500/20 dark:border-red-500/60\"\n            : \"border-zinc-300 dark:border-zinc-700\",\n          className,\n        )}\n        {...props}\n      />\n      {message || counting ? (\n        <div className=\"flex items-start justify-between gap-3\">\n          {message ? (\n            <p id={messageId} className={invalid ? \"text-sm text-red-600 dark:text-red-400\" : \"text-sm text-zinc-500 dark:text-zinc-400\"}>\n              {message}\n            </p>\n          ) : (\n            <span />\n          )}\n          {counting ? (\n            <p\n              id={counterId}\n              className={cx(\n                \"shrink-0 text-xs tabular-nums\",\n                remaining === 0 ? \"font-medium text-red-600 dark:text-red-400\" : \"text-zinc-500 dark:text-zinc-400\",\n              )}\n            >\n              <span aria-hidden=\"true\">\n                {length}/{maxLength}\n              </span>\n              <span className=\"sr-only\">\n                {length} of {maxLength} characters used.\n              </span>\n            </p>\n          ) : null}\n        </div>\n      ) : null}\n      {counting ? (\n        <span role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n          {announcement}\n        </span>\n      ) : null}\n    </div>\n  );\n}\n"
    }
  ],
  "example": {
    "content": "import { Textarea } from \"@/components/seamless/ui/textarea\";\n\nexport default function TextareaExample() {\n  return (\n    <form className=\"grid w-full max-w-sm gap-4\">\n      <Textarea\n        label=\"Bio\"\n        placeholder=\"A sentence or two about you\"\n        description=\"Shown on your public profile.\"\n        maxLength={160}\n        autoGrow\n      />\n      <Textarea label=\"Release notes\" defaultValue=\"Fixed\" error=\"Describe at least one change.\" />\n      <Textarea label=\"Internal notes\" defaultValue=\"Locked while the review is open.\" disabled />\n    </form>\n  );\n}\n"
  }
}
