{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"textarea","title":"Textarea","description":"Base UI-backed multi-line text control.","dependencies":["@base-ui/react@1.7.0"],"registryDependencies":["@siteplane/base","@siteplane/input"],"files":[{"path":"packages/ui/src/components/textarea/textarea.tsx","content":"\"use client\";\n\nimport { Field as FieldPrimitive } from \"@base-ui/react/field\";\nimport { mergeProps } from \"@base-ui/react/merge-props\";\nimport type * as React from \"react\";\n\nimport {\n  type SiteplaneControlSize,\n  useResolvedControlSize,\n} from \"../../system/provider/siteplane-ui-provider\";\nimport {\n  formControlSurfaceClassNames,\n  formControlTextClassNames,\n  surfaceClassNames,\n  type FormControlSurface,\n  type FormControlTextSize,\n} from \"../../system/styles/surface\";\nimport { cn } from \"../../system/utils/cn\";\nimport { slotClass } from \"../../system/utils/slot-class\";\nimport { useFieldLabelPlacement } from \"../field\";\n\nimport type { KitTextareaProps } from \"./textarea-internal\";\n\nimport \"./textarea.css\";\n\nexport type TextareaProps = React.ComponentPropsWithoutRef<\"textarea\"> &\n  React.RefAttributes<HTMLTextAreaElement> & {\n    size?: SiteplaneControlSize | undefined;\n    textSize?: FormControlTextSize | undefined;\n    variant?: FormControlSurface | undefined;\n    unstyled?: boolean | undefined;\n    /**\n     * The least height as a number of text lines, fractions allowed, instead of\n     * the minimum of the size. The textarea still grows with its content. The\n     * native `rows` cannot do this: a textarea that sizes to its content\n     * ignores it.\n     */\n    minLines?: number | undefined;\n    /**\n     * The most height as a number of text lines, fractions allowed: the\n     * textarea grows with its content up to it, then scrolls. Unset, it grows\n     * without a limit. `minLines` wins where the two disagree.\n     */\n    maxLines?: number | undefined;\n  };\n\nexport function Textarea(ownProps: TextareaProps): React.ReactElement {\n  // InputGroupTextarea passes the internal input-group profile through\n  // KitTextarea (textarea-internal.ts), the same function.\n  const {\n    className,\n    size,\n    textSize,\n    variant = \"default\",\n    unstyled = false,\n    profile = \"default\",\n    insetEnd = false,\n    minLines,\n    maxLines,\n    ref,\n    ...props\n  } = ownProps as KitTextareaProps;\n  const resolvedSize = useResolvedControlSize(size);\n  const resolvedTextSize =\n    textSize ??\n    (resolvedSize === \"lg\" || resolvedSize === \"xl\" ? \"base\" : undefined);\n  // A floating Field lays out the textarea it holds (design-rework.css,\n  // \"Floating label fields\"); a textarea inside an InputGroup keeps the\n  // group's layout. A blank placeholder lets CSS tell an empty field.\n  const floating =\n    useFieldLabelPlacement() === \"floating\" && profile !== \"input-group\";\n  const textareaProps =\n    floating && props.placeholder === undefined\n      ? { ...props, placeholder: \" \" }\n      : props;\n\n  const isLineCount = (value: number | undefined): value is number =>\n    value !== undefined && Number.isFinite(value) && value > 0;\n  const hasMinLines = isLineCount(minLines);\n  const hasMaxLines = isLineCount(maxLines);\n  // The line counts are run-time values. The stylesheet turns them into\n  // heights (design-rework.css) and keeps a height where a policy blocks the\n  // style attribute: 2.5 lines for minLines, no limit for maxLines.\n  const lineStyle =\n    hasMinLines || hasMaxLines\n      ? ({\n          ...(hasMinLines ? { \"--textarea-min-lines\": minLines } : {}),\n          ...(hasMaxLines ? { \"--textarea-max-lines\": maxLines } : {}),\n        } as React.CSSProperties)\n      : undefined;\n\n  return (\n    <span\n      className={\n        cn(\n          \"sp-textarea-control sp-base-textarea\",\n          !unstyled && surfaceClassNames.formControl,\n          !unstyled && formControlSurfaceClassNames[variant],\n          resolvedTextSize && formControlTextClassNames[resolvedTextSize],\n          className,\n        ) || undefined\n      }\n      data-foundation=\"textarea\"\n      data-inset-end={(profile === \"input-group\" && insetEnd) || undefined}\n      data-label-placement={floating ? \"floating\" : undefined}\n      data-max-lines={hasMaxLines ? maxLines : undefined}\n      data-min-lines={hasMinLines ? minLines : undefined}\n      data-profile={profile}\n      data-size={resolvedSize}\n      data-slot=\"textarea-control\"\n      // Only an explicit textSize replaces the size-based field text.\n      data-text-size={textSize}\n      data-unstyled={unstyled || undefined}\n      data-variant={variant}\n      style={lineStyle}\n    >\n      <FieldPrimitive.Control\n        ref={ref}\n        value={props.value}\n        defaultValue={props.defaultValue}\n        disabled={props.disabled}\n        id={props.id}\n        name={props.name}\n        render={(defaultProps: React.ComponentProps<\"textarea\">) => (\n          <textarea\n            className={cn(\n              // Field's own props carry no slot; the app's may.\n              slotClass(\"textarea\", textareaProps),\n              \"field-sizing-content w-full outline-none disabled:cursor-not-allowed\",\n            )}\n            data-slot=\"textarea\"\n            {...mergeProps(defaultProps, textareaProps)}\n          />\n        )}\n      />\n    </span>\n  );\n}\n\nexport { FieldPrimitive };\n","type":"registry:ui","target":"~/src/siteplane/components/textarea/textarea.tsx"},{"path":"packages/ui/src/components/textarea/textarea-internal.ts","content":"import type * as React from \"react\";\n\nimport { Textarea, type TextareaProps } from \"./textarea\";\n\n/*\n * Kit-internal Textarea. Not public API: the textarea index does not export\n * this module, and its names and profiles may change in any release.\n * InputGroupTextarea renders its textarea with the input-group profile.\n */\nexport type TextareaProfile = \"default\" | \"input-group\";\n\nexport type KitTextareaProps = TextareaProps & {\n  profile?: TextareaProfile;\n  /**\n   * In the input-group profile: the group keeps the end of the textarea clear\n   * for an action over its corner (InputGroup `insetEnd`).\n   */\n  insetEnd?: boolean | undefined;\n};\n\n/** Textarea with the kit's composition profile; the same component. */\nexport const KitTextarea = Textarea as (\n  props: KitTextareaProps,\n) => React.ReactElement;\n","type":"registry:ui","target":"~/src/siteplane/components/textarea/textarea-internal.ts"},{"path":"packages/ui/src/components/textarea/textarea.css","content":"@layer theme, base, components, siteplane, utilities;\n\n@layer siteplane {\n  .sp-textarea-control {\n    --sp-semantic-color-background: var(--color-white);\n    --sp-semantic-color-border: rgb(0 0 0 / 8%);\n    --sp-semantic-color-destructive: var(--color-red-500);\n    --sp-semantic-color-foreground: var(--color-neutral-800);\n    --sp-semantic-color-input: rgb(0 0 0 / 10%);\n    --sp-semantic-color-ring: var(--color-neutral-400);\n    --input: rgb(0 0 0 / 10%);\n    --ring: var(--color-neutral-400);\n    --textarea-resize-handle-color: var(--input);\n    --textarea-resize-handle-color-focus: var(--ring);\n    --textarea-surface-subtle: rgb(0 0 0 / 4%);\n  }\n\n  .sp-base-textarea[data-profile=\"input-group\"] {\n    display: contents;\n  }\n\n  .sp-base-textarea[data-profile=\"input-group\"]::before {\n    display: none;\n  }\n\n  /* The resize grip is the kit base of every textarea (design-rework.css). */\n  .sp-base-textarea[data-profile=\"input-group\"] .sp-textarea {\n    min-height: calc(var(--spacing) * 20.5);\n    border-radius: 0;\n    padding-block: calc(var(--spacing) * 2 - 1px);\n  }\n\n  @media (width < 40rem) {\n    .sp-base-textarea[data-profile=\"input-group\"] .sp-textarea {\n      min-height: calc(var(--spacing) * 23.5);\n    }\n  }\n\n  .sp-textarea-control:not([data-unstyled]):has(:disabled) {\n    background-color: var(--textarea-surface-subtle);\n  }\n\n  textarea.sp-textarea:focus-visible {\n    --textarea-resize-handle-color: var(--textarea-resize-handle-color-focus);\n  }\n\n  textarea.sp-textarea::-webkit-resizer {\n    border: 0;\n    background:\n      linear-gradient(\n          135deg,\n          transparent calc(50% - 0.65px),\n          var(--textarea-resize-handle-color) 0 calc(50% + 0.65px),\n          transparent 0\n        )\n        right 3px bottom 3px / 9px 9px no-repeat,\n      linear-gradient(\n          135deg,\n          transparent calc(50% - 0.65px),\n          var(--textarea-resize-handle-color) 0 calc(50% + 0.65px),\n          transparent 0\n        )\n        right 3px bottom 3px / 4.75px 4.75px no-repeat;\n  }\n\n  [data-radius=\"none\"] textarea.sp-textarea::-webkit-resizer,\n  [data-radius=\"sm\"] textarea.sp-textarea::-webkit-resizer {\n    background:\n      linear-gradient(\n          135deg,\n          transparent calc(50% - 0.65px),\n          var(--textarea-resize-handle-color) 0 calc(50% + 0.65px),\n          transparent 0\n        )\n        right 3px bottom 3px / 9px 9px no-repeat,\n      linear-gradient(\n          135deg,\n          transparent calc(50% - 0.65px),\n          var(--textarea-resize-handle-color) 0 calc(50% + 0.65px),\n          transparent 0\n        )\n        right 3px bottom 3px / 4.75px 4.75px no-repeat;\n  }\n\n  [data-radius=\"lg\"] textarea.sp-textarea::-webkit-resizer {\n    background:\n      linear-gradient(\n          135deg,\n          transparent calc(50% - 0.65px),\n          var(--textarea-resize-handle-color) 0 calc(50% + 0.65px),\n          transparent 0\n        )\n        right 5px bottom 5px / 9px 9px no-repeat,\n      linear-gradient(\n          135deg,\n          transparent calc(50% - 0.65px),\n          var(--textarea-resize-handle-color) 0 calc(50% + 0.65px),\n          transparent 0\n        )\n        right 5px bottom 5px / 4.75px 4.75px no-repeat;\n  }\n\n  [data-radius=\"xl\"] textarea.sp-textarea::-webkit-resizer {\n    background:\n      linear-gradient(\n          135deg,\n          transparent calc(50% - 0.65px),\n          var(--textarea-resize-handle-color) 0 calc(50% + 0.65px),\n          transparent 0\n        )\n        right 6px bottom 6px / 8.5px 8.5px no-repeat,\n      linear-gradient(\n          135deg,\n          transparent calc(50% - 0.65px),\n          var(--textarea-resize-handle-color) 0 calc(50% + 0.65px),\n          transparent 0\n        )\n        right 6px bottom 6px / 4.75px 4.75px no-repeat;\n  }\n\n  .dark .sp-textarea-control,\n  [data-theme=\"dark\"] .sp-textarea-control {\n    --sp-semantic-color-background: color-mix(\n      in srgb,\n      var(--color-neutral-950) 95%,\n      var(--color-white)\n    );\n    --sp-semantic-color-border: rgb(255 255 255 / 6%);\n    --sp-semantic-color-destructive: color-mix(\n      in srgb,\n      var(--color-red-500) 90%,\n      var(--color-white)\n    );\n    --sp-semantic-color-foreground: var(--color-neutral-100);\n    --sp-semantic-color-input: rgb(255 255 255 / 8%);\n    --sp-semantic-color-ring: var(--color-neutral-500);\n    --input: rgb(255 255 255 / 8%);\n    --ring: var(--color-neutral-500);\n    --textarea-surface-subtle: rgb(255 255 255 / 4%);\n  }\n\n  :is(.dark, [data-theme=\"dark\"])\n    .sp-textarea-control:not([data-unstyled]):has(:disabled) {\n    background-color: color-mix(\n      in oklab,\n      var(--sp-semantic-color-input) 32%,\n      transparent\n    );\n  }\n}\n","type":"registry:ui","target":"~/src/siteplane/components/textarea/textarea.css"},{"path":"packages/ui/src/components/textarea/index.ts","content":"export * from \"./textarea\";\n","type":"registry:ui","target":"~/src/siteplane/components/textarea/index.ts"}],"type":"registry:ui","docs":"https://ui.siteplane.io/docs-markdown/components/textarea.md","categories":["Forms & selection"],"meta":{"avoidWhen":["Avoid for a single line; use Input.","Avoid for formatted text: Textarea edits plain text."],"description":"Base UI-backed multi-line text control.","knownLimitations":["There is no autoGrow prop: the field grows with its content through field-sizing: content, where the browser supports it, and shows no resize grip there; elsewhere it keeps a vertical grip."],"providerNotes":[],"releaseStatus":"stable","searchTerms":["textarea"],"useWhen":["Use for text of several lines, such as a message or a description, that grows with its content."]}}