diff --git a/src/components/drill/DrillStackProvider.test.tsx b/src/components/drill/DrillStackProvider.test.tsx
new file mode 100644
index 0000000..1c6467a
--- /dev/null
+++ b/src/components/drill/DrillStackProvider.test.tsx
@@ -0,0 +1,45 @@
+// @vitest-environment happy-dom
+(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
+
+import { describe, it, expect } from "vitest";
+import { renderHook, act } from "@testing-library/react";
+import type { ReactNode } from "react";
+import {
+ DrillStackProvider,
+ useDrillStack,
+} from "@/components/drill/DrillStackProvider";
+
+function wrapper({ children }: { children: ReactNode }) {
+ return {children};
+}
+
+describe("DrillStackProvider", () => {
+ it("starts with an empty stack", () => {
+ const { result } = renderHook(() => useDrillStack(), { wrapper });
+ expect(result.current.stack).toEqual([]);
+ expect(result.current.openPeek).toBeInstanceOf(Function);
+ expect(result.current.closeTop).toBeInstanceOf(Function);
+ });
+
+ it("openPeek pushes one entry; closeTop pops it", () => {
+ const { result } = renderHook(() => useDrillStack(), { wrapper });
+ act(() => result.current.openPeek({ kind: "payer", payerId: "SKCO0" }));
+ expect(result.current.stack).toEqual([
+ { kind: "payer", payerId: "SKCO0" },
+ ]);
+ act(() => result.current.closeTop());
+ expect(result.current.stack).toEqual([]);
+ });
+
+ it("openPeek replaces the previous peek when called twice", () => {
+ const { result } = renderHook(() => useDrillStack(), { wrapper });
+ act(() => result.current.openPeek({ kind: "payer", payerId: "A" }));
+ // The hook only governs peeks (the bottom drawer is owned by the
+ // page, URL-backed, and lives outside this stack). When openPeek
+ // is called while a peek is already on the stack, the new peek
+ // replaces the previous one rather than stacking above it.
+ act(() => result.current.openPeek({ kind: "rule", rule: "R050" }));
+ expect(result.current.stack).toHaveLength(1);
+ expect(result.current.stack[0]).toEqual({ kind: "rule", rule: "R050" });
+ });
+});
diff --git a/src/components/drill/DrillStackProvider.tsx b/src/components/drill/DrillStackProvider.tsx
new file mode 100644
index 0000000..4ffbc7b
--- /dev/null
+++ b/src/components/drill/DrillStackProvider.tsx
@@ -0,0 +1,53 @@
+import { createContext, useContext, useMemo, type ReactNode } from "react";
+import { create } from "zustand";
+
+export type PeekPayload =
+ | { kind: "payer"; payerId: string }
+ | { kind: "rule"; rule: string };
+
+interface DrillState {
+ stack: PeekPayload[];
+ openPeek: (p: PeekPayload) => void;
+ closeTop: () => void;
+ closeAll: () => void;
+}
+
+// One zustand store per provider instance (factory) so multiple
+// providers (e.g. in tests) don't share state.
+function makeStore() {
+ return create((set) => ({
+ stack: [],
+ openPeek: (p) =>
+ set((s) => ({
+ // Cap at 2 levels total: one drawer + one peek. When called and
+ // the stack already has one peek, replace it.
+ stack: s.stack.length >= 1 ? [p] : [p],
+ })),
+ closeTop: () => set((s) => ({ stack: s.stack.slice(0, -1) })),
+ closeAll: () => set({ stack: [] }),
+ }));
+}
+
+type StoreApi = ReturnType;
+
+const Ctx = createContext(null);
+
+export function DrillStackProvider({ children }: { children: ReactNode }) {
+ // useMemo so the store instance is stable across renders.
+ const store = useMemo(makeStore, []);
+ return {children};
+}
+
+export function useDrillStack() {
+ const store = useContext(Ctx);
+ if (!store) throw new Error("useDrillStack must be used within DrillStackProvider");
+ // Subscribe to just `stack` so consumers re-render only on stack
+ // changes (not on every state update).
+ const stack = store((s) => s.stack);
+ return {
+ stack,
+ openPeek: store.getState().openPeek,
+ closeTop: store.getState().closeTop,
+ closeAll: store.getState().closeAll,
+ };
+}