diff --git a/openspec/changes/archive/2026-04-01-empty-state-component/.openspec.yaml b/openspec/changes/archive/2026-04-01-empty-state-component/.openspec.yaml
new file mode 100644
index 0000000..0f52803
--- /dev/null
+++ b/openspec/changes/archive/2026-04-01-empty-state-component/.openspec.yaml
@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-04-01
diff --git a/openspec/changes/archive/2026-04-01-empty-state-component/design.md b/openspec/changes/archive/2026-04-01-empty-state-component/design.md
new file mode 100644
index 0000000..0e1ae55
--- /dev/null
+++ b/openspec/changes/archive/2026-04-01-empty-state-component/design.md
@@ -0,0 +1,74 @@
+## Context
+
+The Mori component library has Button, Input, Card, Badge, Skeleton, Dialog, and Navigation, but no standard pattern for zero-data screens. Every time a ShiftMate or FaxBridge view renders an empty list (no shifts scheduled, no faxes received), the product team has to write one-off layouts. This produces inconsistent icon sizes, copy tone, and spacing across views. A shared `EmptyState` component fixes this by codifying the visual pattern once.
+
+The existing `localisation-copy` spec already includes `empty.default` and `empty.search` keys; we extend it with `empty.description` to provide a generic fallback description.
+
+## Goals / Non-Goals
+
+**Goals:**
+- Single `EmptyState` component covering all zero-data scenarios in both product repos
+- Follows the existing Mori copy-paste component model (shadcn/ui pattern — products own the file)
+- All design tokens referenced via CSS variables; no hardcoded values
+- Locale-aware default copy via `useCopy()` (heading and description fallbacks)
+- Optional Lucide icon slot with consistent sizing (48×48px, `--color-ink-tertiary`)
+- Optional CTA using the existing `Button` component
+- Vitest tests covering all prop combinations and locale fallbacks
+- Showcase section demonstrating icon / no-icon and CTA / no-CTA variants
+
+**Non-Goals:**
+- Animated illustrations or Lottie files (MVP uses icons only)
+- Context-specific copy built into the component (products pass their own heading/description)
+- Full-page error states (separate concern; `EmptyState` is for zero-data, not error conditions)
+
+## Decisions
+
+### 1. Explicit `heading` prop vs. locale-derived default
+
+**Decision:** `heading` is a required prop. Products always pass their own Japanese heading (e.g., `まだシフトがありません`). The component does NOT fall back to `empty.default` copy — that copy is used by product code directly when a generic message is acceptable.
+
+**Rationale:** Empty-state messages are data-domain specific ("No shifts yet" vs. "No faxes received"). Baking a generic fallback into the component would encourage lazy, context-free copy. Requiring `heading` forces product teams to write meaningful messages at each call site.
+
+**Alternative considered:** `heading` optional with `useCopy().empty.default` fallback. Rejected because it hides poor copy behind a reasonable-looking default; code review can't tell whether the heading was intentional.
+
+---
+
+### 2. Icon type — `LucideIcon` component vs. string name
+
+**Decision:** Accept a Lucide icon component reference (`icon?: LucideIcon`) rather than a string icon name.
+
+**Rationale:** Component references are tree-shakeable and type-safe. String names would require a runtime registry or dynamic import — unnecessary complexity for MVP.
+
+**Alternative considered:** `iconName: string` with a Lucide dynamic import. Rejected: adds runtime complexity and breaks tree-shaking.
+
+---
+
+### 3. CTA — render `Button` internally vs. accept ReactNode
+
+**Decision:** Accept a structured `action?: { label: string; onClick: () => void }` prop and render an internal `Button` with `variant="primary"` and `size="md"`.
+
+**Rationale:** Constraining the CTA to a `Button` keeps the visual contract tight — no accidental link-style or ghost CTAs in empty states. Products that need exotic CTA behaviour can omit the prop and render their own button below the component.
+
+**Alternative considered:** `action?: React.ReactNode` — maximum flexibility. Rejected for MVP because it opens the door to inconsistent styling.
+
+---
+
+### 4. `localisation-copy` extension — `empty.description`
+
+**Decision:** Add `empty.description` to `MoriCopy` across all three locales, providing a generic "nothing here yet" message for contexts where a description is needed but no product-specific copy exists.
+
+**Rationale:** The existing `empty.default` serves the heading role. A separate description key avoids overloading the same string in two UI positions with different typographic treatment.
+
+## Risks / Trade-offs
+
+- [Heading always required] → Product teams may pass empty strings to bypass the requirement. Mitigated by TypeScript `string` type (not `string | undefined`) which at least ensures intention, and code-review convention.
+- [Fixed CTA variant] → A future design requirement may need a ghost or secondary CTA in empty states. Mitigation: the `action` prop struct can be extended with `variant` later without breaking the interface.
+- [Single CTA only] → Some empty states may need two actions. Out of scope for MVP; product can render a second `Button` below the component.
+
+## Migration Plan
+
+No existing code to migrate — this is a net-new component. Products copy `src/components/ui/empty-state.tsx` into their own `components/ui/` directory per the standard Mori copy-paste model.
+
+## Open Questions
+
+None — all decisions are resolved above.
diff --git a/openspec/changes/archive/2026-04-01-empty-state-component/proposal.md b/openspec/changes/archive/2026-04-01-empty-state-component/proposal.md
new file mode 100644
index 0000000..7bc78d2
--- /dev/null
+++ b/openspec/changes/archive/2026-04-01-empty-state-component/proposal.md
@@ -0,0 +1,30 @@
+## Why
+
+Every data view in ShiftMate and FaxBridge — shift lists, fax logs, search results — can return zero items, and there is currently no standard pattern for rendering that state. Without a shared component, each product team would invent ad-hoc "no data" screens that break visual consistency and miss the Japanese UX expectations (friendly, encouraging tone rather than a cold system message).
+
+## What Changes
+
+- New `EmptyState` component in `src/components/ui/empty-state.tsx`
+- Optional Lucide icon slot, required heading, optional description, optional CTA button
+- Locale-aware copy via `useCopy()` (ja / en / zh-Hans)
+- Vitest unit tests covering all prop combinations and locale output
+- New `EmptyState` section added to the component showcase
+
+## Capabilities
+
+### New Capabilities
+
+- `empty-state`: A composable empty-state display component with icon, heading, description, and optional CTA — used whenever a data view returns zero results.
+
+### Modified Capabilities
+
+- `localisation-copy`: Add empty-state copy keys (`emptyState.heading` and `emptyState.description`) to the `MoriCopy` catalogue for all three locales.
+
+## Impact
+
+- **New file**: `src/components/ui/empty-state.tsx`
+- **New test file**: `src/components/ui/__tests__/empty-state.test.tsx`
+- **Modified**: `src/lib/locale/copy.ts` — extend `MoriCopy` type with `emptyState` copy keys
+- **Modified**: `src/index.ts` — export `EmptyState` and its props type
+- **Modified**: `showcase/src/App.tsx` and `showcase/src/sections/` — add EmptyState demo section
+- **Dependencies**: Lucide React (already installed), existing `Button` component, existing `useCopy()` hook
diff --git a/openspec/changes/archive/2026-04-01-empty-state-component/specs/empty-state/spec.md b/openspec/changes/archive/2026-04-01-empty-state-component/specs/empty-state/spec.md
new file mode 100644
index 0000000..f2412b6
--- /dev/null
+++ b/openspec/changes/archive/2026-04-01-empty-state-component/specs/empty-state/spec.md
@@ -0,0 +1,79 @@
+## ADDED Requirements
+
+### Requirement: EmptyState component renders a centred zero-data layout
+`src/components/ui/empty-state.tsx` SHALL export an `EmptyState` React functional component and an `EmptyStateProps` TypeScript interface. The component SHALL render a vertically and horizontally centred layout containing an optional icon, a required heading, an optional description, and an optional CTA button. All colour and spacing values SHALL reference CSS custom property tokens — no hardcoded hex or px values.
+
+#### Scenario: Minimal render with heading only
+- **WHEN** `` is rendered
+- **THEN** the heading text SHALL be visible in the DOM
+- **THEN** no icon, description, or button SHALL be rendered
+
+#### Scenario: Full render with all props
+- **WHEN** `` is rendered
+- **THEN** an SVG icon, the heading, the description, and a button labeled "追加する" SHALL all be present in the DOM
+
+---
+
+### Requirement: Icon slot accepts a Lucide icon component
+The `icon` prop SHALL accept a `LucideIcon` component reference (i.e., `React.ComponentType>`). When provided, the icon SHALL render at 48×48px using `--color-ink-tertiary`. When omitted, no icon element SHALL be rendered.
+
+#### Scenario: Icon renders with correct dimensions
+- **WHEN** `` is rendered
+- **THEN** the rendered SVG element SHALL have `width="48"` and `height="48"` attributes (or equivalent CSS)
+
+#### Scenario: No icon when prop omitted
+- **WHEN** `` is rendered
+- **THEN** no `