1"use client";
2
3import React, { useRef, useCallback, useEffect } from "react";
4
5interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
6 label?: string;
7 hint?: string;
8 error?: string;
9 fullWidth?: boolean;
10 autoResize?: boolean;
11}
12
13export function Textarea({
14 label,
15 hint,
16 error,
17 fullWidth = false,
18 autoResize = false,
19 className = "",
20 id,
21 rows = 3,
22 onChange,
23 ...rest
24}: TextareaProps) {
25 const textareaId = id ?? label?.toLowerCase().replace(/\s+/g, "-");
26 const messageId = textareaId && (error || hint) ? `${textareaId}-message` : undefined;
27 const ref = useRef<HTMLTextAreaElement>(null);
28
29 const resize = useCallback(() => {
30 if (!autoResize || !ref.current) return;
31 ref.current.style.height = "auto";
32 ref.current.style.height = ref.current.scrollHeight + "px";
33 }, [autoResize]);
34
35 useEffect(() => { resize(); }, [resize]);
36
37 const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
38 resize();
39 onChange?.(e);
40 };
41
42 return (
43 <div className={["group/field flex flex-col gap-1.5", fullWidth ? "w-full" : ""].join(" ")}>
44 {label && (
45 <label
46 htmlFor={textareaId}
47 className="text-[12.5px] font-medium text-neutral-700 tracking-[-0.005em] transition-colors duration-200 group-focus-within/field:text-neutral-950"
48 >
49 {label}
50 </label>
51 )}
52 <textarea
53 ref={ref}
54 id={textareaId}
55 rows={rows}
56 onChange={handleChange}
57 className={[
58 "field rounded-xl px-4 py-3 text-[13.5px] text-neutral-900 resize-none scroll-thin",
59 "placeholder:text-neutral-400 placeholder:transition-colors placeholder:duration-200 focus:placeholder:text-neutral-300",
60 "disabled:opacity-40 disabled:cursor-not-allowed",
61 fullWidth ? "w-full" : "",
62 className,
63 ]
64 .filter(Boolean)
65 .join(" ")}
66 aria-invalid={error ? true : undefined}
67 aria-describedby={messageId}
68 {...rest}
69 />
70 {error && (
71 <p id={messageId} role="alert" className="text-[11.5px] text-red-600 font-medium flex items-center gap-1 mt-0.5">
72 <svg width="11" height="11" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
73 <circle cx="12" cy="12" r="10" />
74 <path strokeLinecap="round" d="M12 8v4m0 4h.01" />
75 </svg>
76 {error}
77 </p>
78 )}
79 {hint && !error && (
80 <p id={messageId} className="text-[11.5px] text-neutral-500 mt-0.5">{hint}</p>
81 )}
82 </div>
83 );
84}