diff --git a/frontend/cntr/CountdownTimer/CountdownTimer.test.tsx b/frontend/cntr/CountdownTimer/CountdownTimer.test.tsx
new file mode 100644
index 0000000..dbc06b6
--- /dev/null
+++ b/frontend/cntr/CountdownTimer/CountdownTimer.test.tsx
@@ -0,0 +1,165 @@
+import React from "react";
+
+import {
+ render,
+ screen,
+ act,
+} from "@testing-library/react";
+
+import {
+ CountdownTimer,
+} from "./CountdownTimer";
+
+describe(
+ "CountdownTimer",
+ () => {
+ beforeEach(() => {
+ jest.useFakeTimers();
+
+ jest.setSystemTime(
+ new Date(
+ "2026-01-01T12:00:00Z",
+ ),
+ );
+ });
+
+ afterEach(() => {
+ jest.useRealTimers();
+ });
+
+ it(
+ "renders HH:MM:SS format",
+ () => {
+ render(
+ ,
+ );
+
+ expect(
+ screen.getByText(
+ "01:01:05",
+ ),
+ ).toBeInTheDocument();
+ },
+ );
+
+ it(
+ "turns red when 5 minutes remain",
+ () => {
+ render(
+ ,
+ );
+
+ expect(
+ screen.getByTestId(
+ "countdown-timer",
+ ),
+ ).toHaveClass(
+ "text-red-600",
+ );
+ },
+ );
+
+ it(
+ "calls onExpire exactly once",
+ () => {
+ const onExpire =
+ jest.fn();
+
+ render(
+ ,
+ );
+
+ act(() => {
+ jest.advanceTimersByTime(
+ 5000,
+ );
+ });
+
+ expect(
+ onExpire,
+ ).toHaveBeenCalledTimes(
+ 1,
+ );
+ },
+ );
+
+ it(
+ "updates every second",
+ () => {
+ render(
+ ,
+ );
+
+ act(() => {
+ jest.advanceTimersByTime(
+ 1000,
+ );
+ });
+
+ expect(
+ screen.getByText(
+ "00:00:09",
+ ),
+ ).toBeInTheDocument();
+ },
+ );
+
+ it(
+ "clears interval on unmount",
+ () => {
+ const clearSpy =
+ jest.spyOn(
+ window,
+ "clearInterval",
+ );
+
+ const {
+ unmount,
+ } = render(
+ ,
+ );
+
+ unmount();
+
+ expect(
+ clearSpy,
+ ).toHaveBeenCalled();
+
+ clearSpy.mockRestore();
+ },
+ );
+ },
+);
\ No newline at end of file
diff --git a/frontend/cntr/CountdownTimer/CountdownTimer.tsx b/frontend/cntr/CountdownTimer/CountdownTimer.tsx
new file mode 100644
index 0000000..3dc4e09
--- /dev/null
+++ b/frontend/cntr/CountdownTimer/CountdownTimer.tsx
@@ -0,0 +1,151 @@
+import React, {
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from "react";
+
+export interface CountdownTimerProps {
+ endsAt: Date | string;
+ onExpire?: () => void;
+}
+
+const WARNING_THRESHOLD_SECONDS = 300;
+
+function getRemainingSeconds(
+ endsAt: Date | string,
+): number {
+ const endTime =
+ new Date(endsAt).getTime();
+
+ const now = Date.now();
+
+ return Math.max(
+ 0,
+ Math.floor(
+ (endTime - now) / 1000,
+ ),
+ );
+}
+
+function formatTime(
+ totalSeconds: number,
+): string {
+ const hours = Math.floor(
+ totalSeconds / 3600,
+ );
+
+ const minutes = Math.floor(
+ (totalSeconds % 3600) / 60,
+ );
+
+ const seconds =
+ totalSeconds % 60;
+
+ return [
+ hours,
+ minutes,
+ seconds,
+ ]
+ .map((value) =>
+ String(value).padStart(
+ 2,
+ "0",
+ ),
+ )
+ .join(":");
+}
+
+export const CountdownTimer =
+ ({
+ endsAt,
+ onExpire,
+ }: CountdownTimerProps) => {
+ const [
+ remainingSeconds,
+ setRemainingSeconds,
+ ] = useState(() =>
+ getRemainingSeconds(
+ endsAt,
+ ),
+ );
+
+ const hasExpiredRef =
+ useRef(false);
+
+ useEffect(() => {
+ setRemainingSeconds(
+ getRemainingSeconds(
+ endsAt,
+ ),
+ );
+
+ hasExpiredRef.current =
+ false;
+ }, [endsAt]);
+
+ useEffect(() => {
+ const tick = () => {
+ const remaining =
+ getRemainingSeconds(
+ endsAt,
+ );
+
+ setRemainingSeconds(
+ remaining,
+ );
+
+ if (
+ remaining === 0 &&
+ !hasExpiredRef.current
+ ) {
+ hasExpiredRef.current =
+ true;
+
+ onExpire?.();
+ }
+ };
+
+ tick();
+
+ const intervalId =
+ window.setInterval(
+ tick,
+ 1000,
+ );
+
+ return () => {
+ window.clearInterval(
+ intervalId,
+ );
+ };
+ }, [endsAt, onExpire]);
+
+ const formattedTime =
+ useMemo(
+ () =>
+ formatTime(
+ remainingSeconds,
+ ),
+ [remainingSeconds],
+ );
+
+ const isWarning =
+ remainingSeconds <=
+ WARNING_THRESHOLD_SECONDS;
+
+ return (
+
+ {formattedTime}
+
+ );
+ };
+
+export default CountdownTimer;
\ No newline at end of file
diff --git a/frontend/cntr/CountdownTimer/index.ts b/frontend/cntr/CountdownTimer/index.ts
new file mode 100644
index 0000000..b0c8bc5
--- /dev/null
+++ b/frontend/cntr/CountdownTimer/index.ts
@@ -0,0 +1,4 @@
+export * from "./CountdownTimer";
+export {
+ default,
+} from "./CountdownTimer";
\ No newline at end of file