diff --git a/src/shared/ui/Button/index.ts b/src/shared/ui/Button/index.ts
new file mode 100644
index 0000000..ee20067
--- /dev/null
+++ b/src/shared/ui/Button/index.ts
@@ -0,0 +1,2 @@
+export { Button } from './ui/Button'
+export type { ButtonVariant, ButtonSize } from './ui/Button'
diff --git a/src/shared/ui/Button/ui/Button.test.tsx b/src/shared/ui/Button/ui/Button.test.tsx
new file mode 100644
index 0000000..48410b8
--- /dev/null
+++ b/src/shared/ui/Button/ui/Button.test.tsx
@@ -0,0 +1,67 @@
+import { describe, it, expect, vi } from 'vitest'
+import { render, screen } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import { Button } from './Button'
+
+describe('Button', () => {
+ describe('rendering', () => {
+ it('renders children', () => {
+ render()
+ expect(screen.getByRole('button', { name: 'Click me' })).toBeInTheDocument()
+ })
+ it('renders as button element', () => {
+ render()
+ expect(screen.getByRole('button')).toBeInTheDocument()
+ })
+ })
+ describe('variants', () => {
+ it('applies primary variant by default', () => {
+ render()
+ expect(screen.getByRole('button')).toHaveClass('bg-burnt-oxide')
+ })
+ it('applies secondary variant', () => {
+ render()
+ expect(screen.getByRole('button')).toHaveClass('bg-slate-indigo')
+ })
+ it('applies outline variant', () => {
+ render()
+ expect(screen.getByRole('button')).toHaveClass('bg-transparent')
+ })
+ it('applies ghost variant', () => {
+ render()
+ expect(screen.getByRole('button')).toHaveClass('bg-ochre-clay')
+ })
+ })
+ describe('sizes', () => {
+ it('applies md size by default', () => {
+ render()
+ expect(screen.getByRole('button')).toHaveClass('px-6', 'py-3')
+ })
+ it('applies sm size', () => {
+ render()
+ expect(screen.getByRole('button')).toHaveClass('px-4', 'py-2')
+ })
+ it('applies lg size', () => {
+ render()
+ expect(screen.getByRole('button')).toHaveClass('px-8', 'py-4')
+ })
+ })
+ describe('interactions', () => {
+ it('calls onClick when clicked', async () => {
+ const onClick = vi.fn()
+ render()
+ await userEvent.click(screen.getByRole('button'))
+ expect(onClick).toHaveBeenCalledOnce()
+ })
+ it('is disabled when disabled prop is set', () => {
+ render()
+ expect(screen.getByRole('button')).toBeDisabled()
+ })
+ })
+ describe('className passthrough', () => {
+ it('merges custom className', () => {
+ render()
+ expect(screen.getByRole('button')).toHaveClass('w-full')
+ })
+ })
+})
diff --git a/src/shared/ui/Button/ui/Button.tsx b/src/shared/ui/Button/ui/Button.tsx
new file mode 100644
index 0000000..c1f256c
--- /dev/null
+++ b/src/shared/ui/Button/ui/Button.tsx
@@ -0,0 +1,48 @@
+import type { ButtonHTMLAttributes, ReactNode } from 'react'
+import { cn } from '$shared/lib'
+
+export type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'ghost'
+export type ButtonSize = 'sm' | 'md' | 'lg'
+
+interface Props extends ButtonHTMLAttributes {
+ /**
+ * Visual variant
+ * @default 'primary'
+ */
+ variant?: ButtonVariant
+ /**
+ * Size preset
+ * @default 'md'
+ */
+ size?: ButtonSize
+ /**
+ * Button content
+ */
+ children: ReactNode
+}
+
+const VARIANTS: Record = {
+ primary: 'bg-burnt-oxide text-ochre-clay',
+ secondary: 'bg-slate-indigo text-ochre-clay',
+ outline: 'bg-transparent text-carbon-black border-carbon-black',
+ ghost: 'bg-ochre-clay text-carbon-black border-carbon-black',
+}
+
+const SIZES: Record = {
+ sm: 'px-4 py-2 text-sm',
+ md: 'px-6 py-3 text-base',
+ lg: 'px-8 py-4 text-lg',
+}
+
+const BASE = 'brutal-border brutal-shadow transition-all duration-200 hover:translate-x-[2px] hover:translate-y-[2px] hover:shadow-[6px_6px_0_var(--carbon-black)] active:translate-x-[4px] active:translate-y-[4px] active:shadow-[4px_4px_0_var(--carbon-black)] uppercase tracking-wider'
+
+/**
+ * Brutalist button with variants and sizes.
+ */
+export function Button({ variant = 'primary', size = 'md', className, children, ...props }: Props) {
+ return (
+
+ )
+}