{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "textarea",
  "type": "registry:ui",
  "title": "Textarea",
  "description": "Several lines of text in the field well that grows with what is written on the settle spring; with a limit, a counter fades in near it and shakes on the refusal spring when writing goes past it.",
  "dependencies": [],
  "registryDependencies": [
    "https://metalui.dev/r/tokens.json"
  ],
  "files": [
    {
      "path": "packages/metalui/src/components/textarea/textarea.tsx",
      "type": "registry:ui",
      "target": "components/metalui/textarea/textarea.tsx",
      "content": "'use client';\n\nimport * as React from 'react';\nimport { Field as BaseField } from '@base-ui/react/field';\n\n/* ─────────────────────────────────────────────────────────\n * TEXTAREA, several lines of text in the field well that grows with what is written\n *\n *   rest      the field well, min rows tall (3), the content type role\n *   focus     the flush green ring\n *   grow      a new line grows the well on the settle spring (no overshoot); deleting shrinks it\n *             the same way; at max rows (8) it stops and scrolls. The text stays pinned to the top.\n *   count     with maxLength, a counter's row grows open below at 80 % (the form error's motion:\n *             settle spring, fading in) and turns red at the limit\n *   refused   typing or pasting past the limit shakes only the counter on the refusal spring\n *             (a nest's reach); the text is left alone and a screen reader hears it once\n *   invalid   a red hairline ring\n *   disabled  40 %\n * Reduce Motion: the height snaps and nothing shakes; the counter still turns red.\n * The height is measured from a hidden mirror of the text, so it can spring between real numbers.\n * ───────────────────────────────────────────────────────── */\n\nexport interface TextareaProps extends Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'rows'> {\n  /** Rows before it starts to grow (3). */\n  minRows?: number;\n  /** Rows before it stops growing and scrolls (8). */\n  maxRows?: number;\n  /** Shows the red ring; also sets aria-invalid. */\n  invalid?: boolean;\n  /** Where the well goes: `className` sits on the well, `style` on the textarea. */\n  className?: string;\n}\n\nconst WELL = 'mu-textarea group/ta relative block box-border rounded-textarea-radius recipe-well-field cursor-text focus-within:focus-ring-flush data-invalid:invalid-ring has-[textarea[data-invalid]]:invalid-ring has-[textarea[data-disabled]]:opacity-textarea-disabled data-disabled:opacity-textarea-disabled data-disabled:cursor-default';\nconst TEXT = 'px-textarea-pad-x py-textarea-pad-y type-content whitespace-pre-wrap break-words';\nconst INPUT = `mu-textarea-input block w-full box-border m-0 border-0 outline-none bg-transparent resize-none ${TEXT} text-field-field-ink caret-field-field-caret placeholder:text-field-field-hint transition-textarea-grow reduced-motion:transition-none disabled:cursor-default`;\nconst MIRROR = `mu-textarea-mirror invisible absolute inset-x-0 top-0 pointer-events-none ${TEXT}`;\nconst COUNT_ROW = 'mu-textarea-count-row textarea-count-row';\nconst COUNT = 'mu-textarea-count pt-textarea-count-gap text-right type-meta tabular-nums text-ink3 data-at-limit:text-red data-refused:textarea-refused';\n\n/* Base UI's field control rendered as a textarea: inside a FormField it takes the label, description,\n * error and the field's states. Typed as the textarea it renders. */\nconst TextareaControl = BaseField.Control as unknown as React.ForwardRefExoticComponent<\n  React.TextareaHTMLAttributes<HTMLTextAreaElement> & { render: React.ReactElement } & React.RefAttributes<HTMLTextAreaElement>\n>;\n\nconst readPx = (style: CSSStyleDeclaration, prop: string) => parseFloat(style.getPropertyValue(prop)) || 0;\n\n/** Several lines of text. It grows with what is written, between minRows and maxRows. */\nexport const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(function Textarea(\n  { minRows, maxRows, invalid, maxLength, value, defaultValue, onChange, onKeyDown, onPaste, className, disabled, style, ...props },\n  forwardedRef,\n) {\n  const inner = React.useRef<HTMLTextAreaElement>(null);\n  React.useImperativeHandle(forwardedRef, () => inner.current!);\n  const mirror = React.useRef<HTMLDivElement>(null);\n  const [text, setText] = React.useState(() => String(value ?? defaultValue ?? ''));\n  const [height, setHeight] = React.useState<number>();\n  const [scrolls, setScrolls] = React.useState(false);\n  const [refusals, setRefusals] = React.useState(0);\n  const countId = React.useId();\n  const current = value !== undefined ? String(value) : text;\n\n  // Fit the well to the mirror, clamped to the rows; the settle spring carries the change.\n  const fit = React.useCallback(() => {\n    const ta = inner.current, m = mirror.current;\n    if (!ta || !m) return;\n    const s = getComputedStyle(ta);\n    const line = parseFloat(s.lineHeight) || readPx(s, '--mu-r-textarea-self-line');\n    const pad = parseFloat(s.paddingTop) + parseFloat(s.paddingBottom);\n    const lo = (minRows ?? readPx(s, '--mu-r-textarea-self-min-rows')) * line + pad;\n    const hi = (maxRows ?? readPx(s, '--mu-r-textarea-self-max-rows')) * line + pad;\n    const natural = m.offsetHeight;\n    setHeight(Math.min(hi, Math.max(lo, natural)));\n    setScrolls(natural > hi);\n  }, [minRows, maxRows]);\n\n  React.useLayoutEffect(fit, [fit, current]);\n  React.useEffect(() => {\n    const m = mirror.current;\n    if (!m || typeof ResizeObserver === 'undefined') return;\n    const ro = new ResizeObserver(fit);\n    ro.observe(m);\n    return () => ro.disconnect();\n  }, [fit]);\n\n  const room = maxLength != null ? maxLength - current.length : Infinity;\n  const showCount = maxLength != null && current.length >= maxLength * readShow();\n  const atLimit = maxLength != null && room <= 0;\n\n  return (\n    <div className=\"mu-textarea-slot block\">\n      <label className={className ? `${WELL} ${className}` : WELL} data-invalid={invalid ? '' : undefined} data-disabled={disabled ? '' : undefined}>\n        <TextareaControl\n          render={<textarea />}\n          ref={inner}\n          value={value}\n          defaultValue={defaultValue}\n          maxLength={maxLength}\n          disabled={disabled}\n          aria-invalid={invalid || undefined}\n          rows={1}\n          className={INPUT}\n          style={{ height, overflowY: scrolls ? 'auto' : 'hidden', ...style }}\n          onChange={(e) => {\n            if (value === undefined) setText(e.target.value);\n            onChange?.(e);\n          }}\n          onKeyDown={(e) => {\n            onKeyDown?.(e);\n            if (maxLength == null || e.metaKey || e.ctrlKey || e.altKey) return;\n            const printable = e.key.length === 1 || e.key === 'Enter';\n            const ta = e.currentTarget;\n            if (printable && room + (ta.selectionEnd - ta.selectionStart) <= 0) setRefusals((n) => n + 1);\n          }}\n          onPaste={(e) => {\n            onPaste?.(e);\n            if (maxLength == null) return;\n            const ta = e.currentTarget;\n            const selected = ta.selectionEnd - ta.selectionStart;\n            if (e.clipboardData.getData('text').length > room + selected) setRefusals((n) => n + 1);\n          }}\n          {...props}\n        />\n        <div ref={mirror} aria-hidden className={MIRROR}>{current + '​'}</div>\n      </label>\n      {maxLength != null && (\n        <div className={COUNT_ROW} data-shown={showCount ? '' : undefined}>\n          {/* The row collapses to nothing; the counter keeps its own spacing inside. */}\n          <div>\n            <div\n              key={refusals}\n              id={countId}\n              className={COUNT}\n              data-at-limit={atLimit ? '' : undefined}\n              data-refused={refusals > 0 ? '' : undefined}\n            >\n              {current.length}/{maxLength}\n              <span className=\"sr-only\" aria-live=\"polite\">{refusals > 0 && atLimit ? `Limit reached, ${maxLength} characters` : ''}</span>\n            </div>\n          </div>\n        </div>\n      )}\n    </div>\n  );\n});\n\n/** The share of the limit at which the counter shows (the recipe's count.show). */\nfunction readShow() {\n  if (typeof document === 'undefined') return 0.8;\n  return parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--mu-r-textarea-count-show')) || 0.8;\n}\n"
    }
  ],
  "docs": "Agent guide: https://metalui.dev/r/textarea.md. SwiftUI: MetalTextarea in the MetalUI Swift package."
}
