{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "textarea", "title": "Textarea", "description": "A hand-drawn textarea with optional auto-grow and resize support.", "dependencies": [ "roughjs" ], "registryDependencies": [ "https://www.bydefaulthuman.fun/r/use-rough.json", "https://www.bydefaulthuman.fun/r/crumble-theme.json", "https://www.bydefaulthuman.fun/r/rough-lib.json", "utils" ], "files": [ { "path": "registry/new-york/ui/textarea.tsx", "content": "\"use client\";\n\nimport {\n useCallback,\n useEffect,\n useRef,\n useState,\n type ChangeEvent,\n type TextareaHTMLAttributes,\n} from \"react\";\nimport { useRough } from \"@/hooks/use-rough\";\nimport {\n resolveRoughVars,\n type CrumbleColorProps,\n type CrumbleTheme,\n} from \"@/lib/rough\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface TextareaProps\n extends TextareaHTMLAttributes,\n CrumbleColorProps {\n autoGrow?: boolean;\n error?: string;\n label?: string;\n theme?: CrumbleTheme;\n}\n\nexport function Textarea({\n autoGrow = false,\n className,\n error,\n fill,\n id,\n label,\n onBlur,\n onChange,\n onFocus,\n stroke,\n strokeMuted,\n theme: themeProp,\n ...props\n}: TextareaProps) {\n const [focused, setFocused] = useState(false);\n const wrapperRef = useRef(null);\n const textareaRef = useRef(null);\n const svgRef = useRef(null);\n const roughStyle = resolveRoughVars({ stroke, strokeMuted, fill });\n const textareaId =\n id ?? `textarea-${label?.toLowerCase().replace(/\\s+/g, \"-\") ?? \"field\"}`;\n\n const { drawRect, theme } = useRough({\n stableId: textareaId,\n svgRef,\n theme: themeProp,\n variant: \"border\",\n });\n\n const draw = useCallback(() => {\n const svg = svgRef.current;\n const wrapper = wrapperRef.current;\n if (!svg || !wrapper) return;\n\n svg.replaceChildren();\n\n const width = wrapper.offsetWidth;\n const height = wrapper.offsetHeight;\n svg.setAttribute(\"width\", String(width));\n svg.setAttribute(\"height\", String(height));\n svg.setAttribute(\"viewBox\", `0 0 ${width} ${height}`);\n\n const currentStroke = error\n ? \"color-mix(in srgb, currentColor 10%, red)\"\n : focused\n ? \"currentColor\"\n : \"color-mix(in srgb, currentColor 40%, transparent)\";\n\n const node = drawRect(1, 1, width - 2, height - 2, {\n fill: \"none\",\n stroke: currentStroke,\n });\n\n if (node) svg.appendChild(node);\n }, [drawRect, error, focused]);\n\n useEffect(() => {\n draw();\n }, [draw, focused, theme]);\n\n useEffect(() => {\n const target = autoGrow ? wrapperRef.current : textareaRef.current;\n if (!target) return;\n\n const observer = new ResizeObserver(() => {\n if (!autoGrow && textareaRef.current && wrapperRef.current) {\n wrapperRef.current.style.height = `${textareaRef.current.offsetHeight}px`;\n }\n draw();\n });\n\n observer.observe(target);\n return () => observer.disconnect();\n }, [autoGrow, draw]);\n\n const handleChange = (event: ChangeEvent) => {\n if (autoGrow && textareaRef.current && wrapperRef.current) {\n textareaRef.current.style.height = \"auto\";\n const nextHeight = textareaRef.current.scrollHeight;\n textareaRef.current.style.height = `${nextHeight}px`;\n wrapperRef.current.style.height = `${nextHeight}px`;\n draw();\n }\n\n onChange?.(event);\n };\n\n return (\n
\n {label ? (\n \n {label}\n \n ) : null}\n\n {autoGrow ? (\n
\n \n {\n setFocused(false);\n onBlur?.(event);\n }}\n onChange={handleChange}\n onFocus={(event) => {\n setFocused(true);\n onFocus?.(event);\n }}\n className=\"w-full resize-none border-none bg-transparent px-3 py-2.5 font-[inherit] text-sm text-foreground outline-none\"\n style={{ display: \"block\" }}\n {...props}\n />\n
\n ) : (\n
\n \n {\n setFocused(false);\n onBlur?.(event);\n }}\n onChange={handleChange}\n onFocus={(event) => {\n setFocused(true);\n onFocus?.(event);\n }}\n className=\"absolute inset-0 h-full w-full resize-y border-none bg-transparent px-3 py-2.5 font-[inherit] text-sm text-foreground outline-none\"\n {...props}\n />\n
\n )}\n\n {error ? {error} : null}\n
\n );\n}\n", "type": "registry:ui", "target": "components/crumble/ui/textarea.tsx" } ], "type": "registry:ui" }