diff --git a/src/components/TextArea/TextArea.spec.tsx b/src/components/TextArea/TextArea.spec.tsx
new file mode 100644
index 0000000..d054ad5
--- /dev/null
+++ b/src/components/TextArea/TextArea.spec.tsx
@@ -0,0 +1,119 @@
+import { Radius, Size } from "@/types";
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { TextArea } from "./TextArea";
+
+describe("TextArea", () => {
+ // Basic Rendering
+ it("should render textarea element", () => {
+ render();
+ expect(screen.getByRole("textbox")).toBeInTheDocument();
+ });
+
+ it("should render with placeholder", () => {
+ render();
+ const textarea = screen.getByPlaceholderText("Enter text");
+ expect(textarea).toBeInTheDocument();
+ });
+
+ // Props Pass-through
+ it("should pass through native textarea attributes", () => {
+ render(
+
+ );
+ const textarea = screen.getByTestId("custom-textarea");
+ expect(textarea).toHaveAttribute("id", "test-textarea");
+ expect(textarea).toHaveAttribute("placeholder", "Test placeholder");
+ expect(textarea).toHaveAttribute("rows", "5");
+ });
+
+ // User Interaction
+ it("should handle user input", async () => {
+ const user = userEvent.setup();
+ render();
+ const textarea = screen.getByTestId("input-textarea");
+
+ await user.type(textarea, "Hello World");
+ expect(textarea).toHaveValue("Hello World");
+ });
+
+ it("should handle onChange events", async () => {
+ const handleChange = jest.fn();
+ const user = userEvent.setup();
+ render();
+
+ const textarea = screen.getByTestId("change-textarea");
+ await user.type(textarea, "Test");
+
+ expect(handleChange).toHaveBeenCalled();
+ });
+
+ // Disabled State
+ it("should be disabled when disabled prop is true", () => {
+ render();
+ const textarea = screen.getByTestId("disabled-textarea");
+ expect(textarea).toBeDisabled();
+ });
+
+ it("should not accept input when disabled", async () => {
+ const user = userEvent.setup();
+ render();
+ const textarea = screen.getByTestId("disabled-textarea");
+
+ await user.type(textarea, "Test");
+ expect(textarea).toHaveValue("");
+ });
+
+ // Size Variants
+ it("should apply correct classes for Size.Xs", () => {
+ render();
+ const textarea = screen.getByTestId("xs-textarea");
+ expect(textarea).toHaveClass("px-2.5", "py-1.5", "text-xs/5");
+ });
+
+ it("should apply correct classes for Size.Lg", () => {
+ render();
+ const textarea = screen.getByTestId("lg-textarea");
+ expect(textarea).toHaveClass("px-4", "py-3", "text-lg/9");
+ });
+
+ // Radius Variants
+ it("should apply correct radius classes", () => {
+ render();
+ const textarea = screen.getByTestId("radius-textarea");
+ expect(textarea).toHaveClass("rounded-md");
+ });
+
+ // Error State
+ it("should apply error styling when error prop is true", () => {
+ render();
+ const textarea = screen.getByTestId("error-textarea");
+ expect(textarea).toHaveClass("border-semantic-destructive");
+ });
+
+ // Resize Behavior
+ it("should apply resize-none class when resize is none", () => {
+ render();
+ const textarea = screen.getByTestId("resize-textarea");
+ expect(textarea).toHaveClass("resize-none");
+ });
+
+ it("should apply resize-y class by default", () => {
+ render();
+ const textarea = screen.getByTestId("resize-textarea");
+ expect(textarea).toHaveClass("resize-y");
+ });
+
+ // Custom ClassName
+ it("should merge custom className with default classes", () => {
+ render();
+ const textarea = screen.getByTestId("custom-textarea");
+ expect(textarea).toHaveClass("custom-class");
+ expect(textarea).toHaveClass("w-full"); // Still has base classes
+ });
+});
diff --git a/src/components/TextArea/TextArea.tsx b/src/components/TextArea/TextArea.tsx
new file mode 100644
index 0000000..1776a12
--- /dev/null
+++ b/src/components/TextArea/TextArea.tsx
@@ -0,0 +1,83 @@
+import radiusStyles from "@/styles/radius";
+import { Radius, Size } from "@/types";
+import { cn } from "@/util/classes";
+import { Field, Textarea as HeadlessTextarea } from "@headlessui/react";
+import clsx from "clsx";
+import { type FC, TextareaHTMLAttributes } from "react";
+
+export interface TextAreaProps extends TextareaHTMLAttributes {
+ size?: Size;
+ radius?: Radius;
+ className?: string;
+ containerClassName?: string;
+ error?: boolean;
+ resize?: "none" | "vertical" | "horizontal" | "both";
+}
+
+const sizeStyles: Record = {
+ [Size.Xs]: clsx("px-2.5 py-1.5", "text-xs/5"),
+ [Size.Sm]: clsx("px-3 py-2", "text-sm/6"),
+ [Size.Md]: clsx("px-3.5 py-2.5", "text-md/7"),
+ [Size.Lg]: clsx("px-4 py-3", "text-lg/9"),
+};
+
+const resizeStyles: Record, string> = {
+ none: "resize-none",
+ vertical: "resize-y",
+ horizontal: "resize-x",
+ both: "resize",
+};
+
+export const TextArea: FC = ({
+ size = Size.Md,
+ radius = Radius.Sm,
+ error = false,
+ resize = "vertical",
+ rows = 3,
+ className,
+ containerClassName,
+ disabled,
+ ...props
+}) => {
+ return (
+
+
+
+ );
+};
+
+TextArea.displayName = "TextArea";
diff --git a/src/components/TextArea/index.ts b/src/components/TextArea/index.ts
new file mode 100644
index 0000000..cc37a6a
--- /dev/null
+++ b/src/components/TextArea/index.ts
@@ -0,0 +1 @@
+export * from "./TextArea";
diff --git a/src/components/index.ts b/src/components/index.ts
index ce0ba31..41ad241 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -15,6 +15,7 @@ export * from "./RichList";
export * from "./Spinner";
export * from "./Stack";
export * from "./Text";
+export * from "./TextArea";
export * from "./TextBadge";
export * from "./Toast";
export * from "./ToastContainer";