useCountdown
Manage timer state for countdowns and intervals.
A hook for creating countdown timers, interval counters, and stopwatches. It handles start, stop, reset, and completion callbacks.
Source Code
View the full hook implementation in the Hook Source Code section below.
Basic Timer
A simple 10-second countdown with start, stop, and reset controls.
10s
"use client";
import { useCountdown } from "@repo/hooks/utility/use-countdown";
import { Button } from "@repo/ui/components/button";
import { Play, Square, RotateCcw } from "lucide-react";
export const Example1 = () => {
// 10 second countdown
const { count, start, stop, reset, isRunning } = useCountdown(10, {
intervalMs: 1000,
});
return (
<div className="flex flex-col items-center gap-4 rounded-lg border p-6">
<div className="font-mono text-6xl font-bold tabular-nums">
{count.toString().padStart(2, "0")}s
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="icon"
onPress={() => start()}
isDisabled={isRunning || count === 0}
aria-label="Start"
>
<Play className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="icon"
onPress={stop}
isDisabled={!isRunning}
aria-label="Stop"
>
<Square className="h-4 w-4 fill-current" />
</Button>
<Button
variant="outline"
size="icon"
onPress={reset}
aria-label="Reset"
>
<RotateCcw className="h-4 w-4" />
</Button>
</div>
</div>
);
};
OTP Resend Timer
A pattern often used in authentication flows. The implementation includes an onComplete callback.
Verify your email
We sent a 6-digit code to your email. Enter it below to verify your account.
•
•
•
•
•
•
"use client";
import { useCountdown } from "@repo/hooks/utility/use-countdown";
import { Button } from "@repo/ui/components/button";
import { useEffect } from "react";
export const Example2 = () => {
// OTP Resend Timer (30 seconds)
const { count, start, reset, isRunning } = useCountdown(30, {
intervalMs: 1000,
onComplete: () => console.log("Can resend now"),
});
// Auto-start on mount
useEffect(() => {
start();
}, [start]);
const handleResend = async () => {
// Simulate API call
console.log("Resending OTP...");
reset();
start();
};
return (
<div className="max-w-sm space-y-4 rounded-lg border p-6">
<div className="space-y-2">
<h3 className="font-medium">Verify your email</h3>
<p className="text-muted-foreground text-sm">
We sent a 6-digit code to your email. Enter it below to
verify your account.
</p>
</div>
<div className="flex justify-center gap-2">
{Array.from({ length: 6 }).map((_, i) => (
<div
key={i}
className="bg-muted h-12 w-10 rounded-lg border text-center text-xl leading-[46px]"
>
•
</div>
))}
</div>
<Button
className="w-full"
size="sm"
onPress={handleResend}
isDisabled={isRunning && count > 0}
>
{count > 0 ? `Resend code in ${count}s` : "Resend code"}
</Button>
</div>
);
};
Session Timer (Stopwatch)
Using the isIncrement option to count up, simulating a live session timer.
Session Duration
00:00:00
Stopped
"use client";
import { useCountdown } from "@repo/hooks/utility/use-countdown";
import { useEffect } from "react";
export const Example3 = () => {
// Event Countdown: Simulate counting down 1 hour (3600s)
// We increment to simulate a "stopwatch" or elapsed time style
const { count, start, isRunning } = useCountdown(0, {
isIncrement: true,
countStop: Infinity,
});
useEffect(() => {
start();
}, [start]);
const formatTime = (seconds: number) => {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = seconds % 60;
return `${h.toString().padStart(2, "0")}:${m
.toString()
.padStart(2, "0")}:${s.toString().padStart(2, "0")}`;
};
return (
<div className="flex flex-col items-center justify-center gap-2 rounded-lg border bg-zinc-950 p-8 text-white">
<div className="text-xs font-medium uppercase tracking-widest text-zinc-400">
Session Duration
</div>
<div className="font-mono text-5xl font-bold tabular-nums">
{formatTime(count)}
</div>
<div className="flex items-center gap-2 text-sm text-zinc-500">
<span
className={`h-2 w-2 rounded-full ${isRunning ? "animate-pulse bg-green-500" : "bg-red-500"}`}
/>
{isRunning ? "Recording Live" : "Stopped"}
</div>
</div>
);
};
API Reference
Hook Signature
function useCountdown(
countStart: number,
options?: UseCountdownOptions,
): UseCountdownReturn;Parameters
| Name | Type | Description |
|---|---|---|
countStart | number | The starting value of the countdown (in seconds/units). |
options | UseCountdownOptions | Configuration options for the countdown. |
UseCountdownOptions
| Name | Type | Default | Description |
|---|---|---|---|
intervalMs | number | 1000 | The interval in milliseconds. |
isIncrement | boolean | false | Whether to increment (count up) instead of decrement. |
countStop | number | 0 | The value at which the countdown stops. |
onComplete | () => void | undefined | Callback function triggered when the countdown completes. |
UseCountdownReturn
| Name | Type | Description |
|---|---|---|
count | number | The current value of the countdown. |
start | (seconds?: number) => void | Starts the countdown. Optionally accepts a value to override the current count. |
stop | () => void | Pauses/Stops the countdown. |
reset | () => void | Resets the countdown to the countStart value. |
isRunning | boolean | Whether the countdown is currently active. |
Hook Source Code
import { useState, useEffect, useRef, useCallback } from "react";
/**
* Options for the useCountdown hook
*/
export interface UseCountdownOptions {
/** Interval in milliseconds (default: 1000) */
intervalMs?: number;
/** Whether to increment instead of decrement (default: false) */
isIncrement?: boolean;
/** Value to stop at (default: 0) */
countStop?: number;
/** Callback when countdown completes */
onComplete?: () => void;
}
/**
* Return type for the useCountdown hook
*/
export interface UseCountdownReturn {
/** Current count value */
count: number;
/** Start the countdown (optionally override duration) */
start: (seconds?: number) => void;
/** Stop/Pause the countdown */
stop: () => void;
/** Reset the countdown to initial value */
reset: () => void;
/** Whether the countdown is currently active */
isRunning: boolean;
}
/**
* A React hook for managing countdowns and timers.
*
* @param countStart - Starting count value (in seconds/units)
* @param options - Configuration options for the countdown
* @returns UseCountdownReturn object with count and control methods
*/
export function useCountdown(
countStart: number,
options: UseCountdownOptions = {},
): UseCountdownReturn {
const {
intervalMs = 1000,
isIncrement = false,
countStop = 0,
onComplete,
} = options;
const [count, setCount] = useState(countStart);
const [isRunning, setIsRunning] = useState(false);
const callbackRef = useRef(onComplete);
const timerRef = useRef<NodeJS.Timeout | null>(null);
// Keep callback ref updated
useEffect(() => {
callbackRef.current = onComplete;
}, [onComplete]);
const stop = useCallback(() => {
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
}
setIsRunning(false);
}, []);
const start = useCallback(
(seconds?: number) => {
if (isRunning) return;
if (seconds !== undefined) {
setCount(seconds);
}
setIsRunning(true);
timerRef.current = setInterval(() => {
setCount((prevCount) => {
const nextCount = isIncrement
? prevCount + 1
: prevCount - 1;
if (
isIncrement
? nextCount >= countStop
: nextCount <= countStop
) {
stop();
callbackRef.current?.();
return countStop;
}
return nextCount;
});
}, intervalMs);
},
[countStop, intervalMs, isIncrement, isRunning, stop],
);
const reset = useCallback(() => {
stop();
setCount(countStart);
}, [countStart, stop]);
// Cleanup on unmount
useEffect(() => {
return () => stop();
}, [stop]);
return {
count,
start,
stop,
reset,
isRunning,
};
}