{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "autosize-textarea",
  "title": "Autosize Textarea",
  "description": "A textarea that grows as you type and stops at a maximum height, then scrolls. Reach for it wherever a fixed-height box is the wrong shape: a chat, message or AI prompt composer; a comment, reply or code-review box; a commit message or pull-request description; a bio, note, changelog, release note or feedback field; a support ticket or contact form; a task or issue description; and any \"tell us more\" field where one line is too small and a tall empty box wastes the page. Common asks it answers: \"auto resize textarea\", \"auto-growing textarea\", \"expanding textarea react\", \"textarea that grows with content\", \"auto height textarea\", \"textarea min rows max rows\", \"chat input that expands\", \"message composer textarea\", \"prompt input that grows\", \"ChatGPT-style input react\", \"shadcn autosize textarea\", \"shadcn textarea auto grow\", \"react-textarea-autosize alternative\", \"autosize textarea without a library\", \"textarea scrollHeight resize\", \"growing text input component\". shadcn/ui's own textarea is a fixed-height styled element with a drag handle in the corner; this replaces that behaviour, and turns the handle off, because the height is now the field's own business. minRows sets the height it sits at when empty, maxRows caps the growth before it starts scrolling. The reason to install it rather than paste the four-line version is that the four-line version is subtly wrong in three ways that only show up on somebody else's screen. It measures a row from the element's real computed line-height — and falls back to the font size where that computes to \"normal\", which is what it computes to unless you set it explicitly, and which parses to NaN and produces a field with no height at all. It adds the border back on a border-box element, because scrollHeight counts content and padding but not border, so the naive arithmetic is short by a pixel or two on every keystroke and the field creeps. And it watches the element rather than the window, so the height is recomputed when a collapsing sidebar, an opening drawer, a resizing split pane or a tab becoming visible rewraps the text — none of which fire a window resize — while deliberately ignoring height changes, since reacting to its own writes would feed the observer straight back into itself. It renders at roughly the right height before hydration through the rows attribute, so there is no first-paint jump in Next.js, and it is correct controlled or uncontrolled: a controlled field re-measures on the value it is given, an uncontrolled one on its own input. It keeps the native <textarea> and forwards a ref to it, so labels, placeholders, autofocus, maxLength, form libraries such as react-hook-form, and native validation all work unchanged. Styled with shadcn tokens (border-input, ring, muted-foreground) so it follows light and dark, and it ships zero dependencies beyond your own cn util — no icon package, one file.",
  "files": [
    {
      "path": "registry/ui/autosize-textarea.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\ninterface AutosizeTextareaProps extends React.ComponentPropsWithoutRef<\"textarea\"> {\n  /** Smallest height, in rows, the field shrinks back to when empty. Defaults to 2. */\n  minRows?: number\n  /** Tallest height, in rows, before the field stops growing and scrolls instead. */\n  maxRows?: number\n}\n\n// useLayoutEffect measures before paint so the field never flashes at the wrong\n// height, but it warns during SSR — fall back to useEffect on the server.\nconst useIsomorphicLayoutEffect =\n  typeof window === \"undefined\" ? React.useEffect : React.useLayoutEffect\n\n/**\n * A textarea that grows with its content and stops at maxRows. Use it for chat\n * and AI prompt composers, comment and reply boxes, commit or PR descriptions,\n * bios, and any \"message\" field where a fixed height either wastes space or hides\n * what was typed. shadcn/ui's textarea is fixed-height; this measures the real\n * line height so it honours your own font and padding, keeps the native element\n * (so form libraries, labels, and validation work unchanged), and adds no\n * dependencies.\n */\nexport const AutosizeTextarea = React.forwardRef<\n  HTMLTextAreaElement,\n  AutosizeTextareaProps\n>(function AutosizeTextarea(\n  { className, minRows = 2, maxRows, value, onChange, rows, ...props },\n  forwardedRef\n) {\n  const innerRef = React.useRef<HTMLTextAreaElement>(null)\n  React.useImperativeHandle(\n    forwardedRef,\n    () => innerRef.current as HTMLTextAreaElement\n  )\n\n  const resize = React.useCallback(() => {\n    const el = innerRef.current\n    if (!el) return\n\n    const styles = window.getComputedStyle(el)\n    const fontSize = parseFloat(styles.fontSize)\n    // line-height resolves to \"normal\" unless it is set explicitly; approximate it.\n    const lineHeight = parseFloat(styles.lineHeight) || fontSize * 1.5\n    const padding =\n      parseFloat(styles.paddingTop) + parseFloat(styles.paddingBottom)\n    // scrollHeight covers content + padding, so border-box heights need the border too.\n    const extra =\n      styles.boxSizing === \"border-box\"\n        ? padding +\n          parseFloat(styles.borderTopWidth) +\n          parseFloat(styles.borderBottomWidth)\n        : 0\n\n    const min = lineHeight * minRows + extra\n    const max = maxRows ? lineHeight * maxRows + extra : Number.POSITIVE_INFINITY\n\n    // Collapse first so scrollHeight reports the content height rather than the\n    // height we set on the previous keystroke — otherwise it can only grow.\n    el.style.height = \"auto\"\n    const target = el.scrollHeight - padding + extra\n    el.style.height = `${Math.min(Math.max(target, min), max)}px`\n    el.style.overflowY = target > max ? \"auto\" : \"hidden\"\n  }, [minRows, maxRows])\n\n  // Re-measure on every controlled value change, and once on mount.\n  useIsomorphicLayoutEffect(resize, [resize, value])\n\n  // A narrower field rewraps its text, which changes the height it needs. Watch\n  // the element rather than the window so a collapsing sidebar, an opening\n  // panel, or a tab becoming visible is caught too — none of those resize the\n  // window. Falls back to the window where ResizeObserver is unavailable.\n  React.useEffect(() => {\n    const el = innerRef.current\n    if (typeof ResizeObserver === \"undefined\" || !el) {\n      window.addEventListener(\"resize\", resize)\n      return () => window.removeEventListener(\"resize\", resize)\n    }\n    // Only width is worth reacting to: the height is ours to set, so responding\n    // to our own write would feed the observer back into itself.\n    let lastWidth = el.clientWidth\n    const observer = new ResizeObserver(() => {\n      if (el.clientWidth === lastWidth) return\n      lastWidth = el.clientWidth\n      resize()\n    })\n    observer.observe(el)\n    return () => observer.disconnect()\n  }, [resize])\n\n  function handleChange(event: React.ChangeEvent<HTMLTextAreaElement>) {\n    onChange?.(event)\n    // Uncontrolled fields get no value prop to react to, so measure here too.\n    resize()\n  }\n\n  return (\n    <textarea\n      ref={innerRef}\n      // Renders at roughly the right height before hydration measures it.\n      rows={rows ?? minRows}\n      value={value}\n      onChange={handleChange}\n      className={cn(\n        \"flex w-full resize-none rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50\",\n        className\n      )}\n      {...props}\n    />\n  )\n})\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}