diff --git a/src/design-system/primitives/Collapsible/Collapsible.module.css b/src/design-system/primitives/Collapsible/Collapsible.module.css
new file mode 100644
index 0000000..35fa720
--- /dev/null
+++ b/src/design-system/primitives/Collapsible/Collapsible.module.css
@@ -0,0 +1,59 @@
+.root {
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ overflow: hidden;
+}
+
+.trigger {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+ padding: 10px 14px;
+ background: var(--bg-raised);
+ border: none;
+ cursor: pointer;
+ text-align: left;
+ font-family: var(--font-body);
+ font-size: 13px;
+ color: var(--text-primary);
+ transition: background 0.15s;
+}
+
+.trigger:hover {
+ background: var(--bg-hover);
+}
+
+.title {
+ font-weight: 500;
+}
+
+.chevron {
+ color: var(--text-muted);
+ font-size: 14px;
+ transition: transform 0.2s;
+ transform: rotate(-90deg);
+}
+
+.chevronOpen {
+ transform: rotate(0deg);
+}
+
+.content {
+ max-height: 0;
+ overflow: hidden;
+ transition: max-height 0.25s ease;
+}
+
+.content[hidden] {
+ display: block;
+ max-height: 0;
+}
+
+.contentOpen {
+ max-height: 2000px;
+}
+
+.inner {
+ padding: 12px 14px;
+}
diff --git a/src/design-system/primitives/Collapsible/Collapsible.test.tsx b/src/design-system/primitives/Collapsible/Collapsible.test.tsx
new file mode 100644
index 0000000..75a6e56
--- /dev/null
+++ b/src/design-system/primitives/Collapsible/Collapsible.test.tsx
@@ -0,0 +1,36 @@
+import { describe, it, expect, vi } from 'vitest'
+import { render, screen } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import { Collapsible } from './Collapsible'
+
+describe('Collapsible', () => {
+ it('renders title', () => {
+ render(Content)
+ expect(screen.getByText('Details')).toBeInTheDocument()
+ })
+
+ it('hides content by default', () => {
+ render(Hidden content)
+ expect(screen.queryByText('Hidden content')).not.toBeVisible()
+ })
+
+ it('shows content when defaultOpen', () => {
+ render(Visible content)
+ expect(screen.getByText('Visible content')).toBeVisible()
+ })
+
+ it('toggles content on click', async () => {
+ const user = userEvent.setup()
+ render(Content)
+ await user.click(screen.getByText('Details'))
+ expect(screen.getByText('Content')).toBeVisible()
+ })
+
+ it('calls onToggle when toggled', async () => {
+ const onToggle = vi.fn()
+ const user = userEvent.setup()
+ render(Content)
+ await user.click(screen.getByText('Details'))
+ expect(onToggle).toHaveBeenCalled()
+ })
+})
diff --git a/src/design-system/primitives/Collapsible/Collapsible.tsx b/src/design-system/primitives/Collapsible/Collapsible.tsx
new file mode 100644
index 0000000..52b2881
--- /dev/null
+++ b/src/design-system/primitives/Collapsible/Collapsible.tsx
@@ -0,0 +1,53 @@
+import { useState, useRef, useEffect } from 'react'
+import styles from './Collapsible.module.css'
+import type { ReactNode } from 'react'
+
+interface CollapsibleProps {
+ title: ReactNode
+ children: ReactNode
+ defaultOpen?: boolean
+ open?: boolean
+ onToggle?: (open: boolean) => void
+ className?: string
+}
+
+export function Collapsible({
+ title,
+ children,
+ defaultOpen = false,
+ open: controlledOpen,
+ onToggle,
+ className,
+}: CollapsibleProps) {
+ const isControlled = controlledOpen !== undefined
+ const [internalOpen, setInternalOpen] = useState(defaultOpen)
+ const open = isControlled ? controlledOpen : internalOpen
+ const contentRef = useRef(null)
+
+ function handleToggle() {
+ const next = !open
+ if (!isControlled) setInternalOpen(next)
+ onToggle?.(next)
+ }
+
+ return (
+
+ )
+}
diff --git a/src/design-system/primitives/Tooltip/Tooltip.module.css b/src/design-system/primitives/Tooltip/Tooltip.module.css
new file mode 100644
index 0000000..ed4f4fd
--- /dev/null
+++ b/src/design-system/primitives/Tooltip/Tooltip.module.css
@@ -0,0 +1,50 @@
+.wrapper {
+ position: relative;
+ display: inline-flex;
+ align-items: center;
+}
+
+.tip {
+ position: absolute;
+ z-index: 100;
+ padding: 5px 9px;
+ background: var(--text-primary);
+ color: var(--bg-surface);
+ font-size: 11px;
+ font-family: var(--font-body);
+ border-radius: var(--radius-sm);
+ white-space: nowrap;
+ pointer-events: none;
+ opacity: 0;
+ transition: opacity 0.15s;
+ box-shadow: var(--shadow-md);
+}
+
+.wrapper:hover .tip {
+ opacity: 1;
+}
+
+/* Positions */
+.top {
+ bottom: calc(100% + 6px);
+ left: 50%;
+ transform: translateX(-50%);
+}
+
+.bottom {
+ top: calc(100% + 6px);
+ left: 50%;
+ transform: translateX(-50%);
+}
+
+.left {
+ right: calc(100% + 6px);
+ top: 50%;
+ transform: translateY(-50%);
+}
+
+.right {
+ left: calc(100% + 6px);
+ top: 50%;
+ transform: translateY(-50%);
+}
diff --git a/src/design-system/primitives/Tooltip/Tooltip.tsx b/src/design-system/primitives/Tooltip/Tooltip.tsx
new file mode 100644
index 0000000..4c7ba90
--- /dev/null
+++ b/src/design-system/primitives/Tooltip/Tooltip.tsx
@@ -0,0 +1,27 @@
+import styles from './Tooltip.module.css'
+import type { ReactNode } from 'react'
+
+type TooltipPosition = 'top' | 'bottom' | 'left' | 'right'
+
+interface TooltipProps {
+ content: ReactNode
+ position?: TooltipPosition
+ children: ReactNode
+ className?: string
+}
+
+export function Tooltip({
+ content,
+ position = 'top',
+ children,
+ className,
+}: TooltipProps) {
+ return (
+
+ {children}
+
+ {content}
+
+
+ )
+}