# Timer

> Compound timer for React and shadcn/ui with variants, sizes, and time formats, plus a useTimer hook for elapsed time.

Source: https://www.cult-ui.com/docs/components/timer

## Example

```tsx title="timer-demo.tsx"
"use client"

import { Pause, Play, RotateCcw } from "lucide-react"

import { Button } from "@/components/ui/button"
import {
  Timer,
  TimerDisplay,
  TimerIcon,
  TimerRoot,
  useTimer,
} from "@/components/ui/timer"

export default function TimerExamples() {
  return (
    <div className="space-y-8 p-6">
      <div className="space-y-4">
        {/* Basic Timer */}
        <div className="space-y-2">
          <h3 className="text-lg font-semibold">Basic Timer</h3>
          <Timer loading={true} />
        </div>

        {/* Variants */}
        <div className="space-y-2">
          <h3 className="text-lg font-semibold">Variants</h3>
          <div className="flex flex-wrap gap-4">
            <Timer variant="default" loading={true} />
            <Timer variant="outline" loading={true} />
            <Timer variant="ghost" loading={true} />
            <Timer variant="destructive" loading={true} />
          </div>
        </div>

        {/* Sizes */}
        <div className="space-y-2">
          <h3 className="text-lg font-semibold">Sizes</h3>
          <div className="flex flex-wrap items-center gap-4">
            <Timer size="sm" loading={true} />
            <Timer size="md" loading={true} />
            <Timer size="lg" loading={true} />
          </div>
        </div>

        {/* Compound Components */}
        <div className="space-y-2">
          <h3 className="text-lg font-semibold">Compound Components</h3>
          <TimerRoot variant="outline" size="lg">
            <TimerIcon loading={true} />
            <TimerDisplay time="01:23" />
          </TimerRoot>
        </div>

        {/* Custom Timer with Controls */}
        <CustomTimerExample />
      </div>
    </div>
  )
}

function CustomTimerExample() {
  const { formattedTime, isRunning, start, stop, reset } = useTimer({
    format: "MM:SS",
  })

  return (
    <div className="space-y-2">
      <h3 className="text-lg font-semibold">Custom Timer with Controls</h3>
      <div className="flex items-center gap-4">
        <TimerRoot variant="outline" size="lg">
          <TimerIcon loading={isRunning} />
          <TimerDisplay time={formattedTime.display} />
        </TimerRoot>

        <div className="flex gap-2">
          <Button
            size="sm"
            variant="outline"
            onClick={isRunning ? stop : start}
          >
            {isRunning ? (
              <Pause className="h-4 w-4" />
            ) : (
              <Play className="h-4 w-4" />
            )}
          </Button>
          <Button size="sm" variant="outline" onClick={reset}>
            <RotateCcw className="h-4 w-4" />
          </Button>
        </div>
      </div>
    </div>
  )
}
```

Timer is a React timer component system for shadcn/ui with compound parts and a useTimer hook. Use it for AI generation progress, recording indicators, workouts, or any interface that shows how long something has been running.

## Installation

### CLI

```bash
npx shadcn@latest add @cult-ui/timer
```

### Manual

**Copy and paste the following code into your project.**

```tsx title="timer.tsx"
"use client"

import React, { useCallback, useEffect, useRef, useState } from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { Clock } from "lucide-react"

import { cn } from "@/lib/utils"

const timerVariants = cva(
  [
    "inline-flex items-center gap-2 rounded-full font-medium transition-all duration-200",
    "",
  ],
  {
    variants: {
      variant: {
        default:
          "bg-background text-foreground border-border border shadow-[0_2px_4px_rgba(0,0,0,0.02),_0px_1px_2px_rgba(0,0,0,0.04)] shadow-[inset_0px_-2.10843px_0px_0px_hsl(var(--muted)),_0px_1.20482px_6.3253px_0px_hsl(var(--muted))]",
        outline:
          "border-input bg-background text-foreground border shadow-[0px_1px_0px_0px_hsla(0,_0%,_0%,_0.02)_inset,_0px_0px_0px_1px_hsla(0,_0%,_0%,_0.02)_inset,_0px_0px_0px_1px_rgba(255,_255,_255,_0.25)]",
        ghost: "text-foreground bg-transparent",
        destructive:
          "bg-destructive/10 text-destructive border-destructive/20 border",
      },
      size: {
        sm: "h-6 gap-1.5 px-2 py-1 text-xs",
        md: "h-7 gap-2 px-2.5 py-1.5 text-sm",
        lg: "h-8 gap-2.5 px-3 py-2 text-base",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "md",
    },
  }
)

const timerIconVariants = cva("transition-transform duration-[2000ms]", {
  variants: {
    size: {
      sm: "h-3 w-3",
      md: "h-3.5 w-3.5",
      lg: "h-4 w-4",
    },
    loading: {
      true: "animate-spin",
      false: "",
    },
  },
  defaultVariants: {
    size: "md",
    loading: false,
  },
})

const timerDisplayVariants = cva("font-mono tracking-tight tabular-nums", {
  variants: {
    size: {
      sm: "text-xs",
      md: "text-sm",
      lg: "text-base",
    },
  },
  defaultVariants: {
    size: "md",
  },
})

export type TimerRootProps = {
  /** Whether the timer is in loading/running state */
  loading?: boolean
} & VariantProps<typeof timerVariants> &
  React.HTMLAttributes<HTMLDivElement>

export type TimerIconProps = {
  /** Custom icon to display instead of default Clock */
  icon?: React.ComponentType<{ className?: string }>
} & VariantProps<typeof timerIconVariants> &
  React.HTMLAttributes<HTMLDivElement>

export type TimerDisplayProps = {
  /** Time value to display */
  time: string
  /** Optional label for accessibility */
  label?: string
} & VariantProps<typeof timerDisplayVariants> &
  React.HTMLAttributes<HTMLDivElement>

export type UseTimerOptions = {
  /** Whether the timer should be running */
  loading?: boolean
  /** Callback fired on each tick with elapsed time */
  onTick?: (seconds: number, milliseconds: number) => void
  /** Whether to reset timer when loading state changes */
  resetOnLoadingChange?: boolean
  /** Time format to use */
  format?: "SS.MS" | "MM:SS" | "HH:MM:SS"
}

export type UseTimerReturn = {
  /** Total elapsed seconds */
  elapsedTime: number
  /** Current milliseconds (0-999) */
  milliseconds: number
  /** Formatted time strings */
  formattedTime: {
    seconds: string
    milliseconds: string
    display: string
  }
  /** Whether timer is currently running */
  isRunning: boolean
  /** Reset timer to 0 */
  reset: () => void
  /** Start the timer */
  start: () => void
  /** Stop the timer */
  stop: () => void
}

/**
 * Root container for timer components
 */
export const TimerRoot = React.forwardRef<HTMLDivElement, TimerRootProps>(
  ({ variant, size, loading, className, children, ...props }, ref) => {
    return (
      <div
        ref={ref}
        className={cn(timerVariants({ variant, size }), className)}
        role="timer"
        aria-live="polite"
        aria-atomic="true"
        {...props}
      >
        {children}
      </div>
    )
  }
)
TimerRoot.displayName = "TimerRoot"

/**
 * Icon component for timer with loading animation
 */
export const TimerIcon = React.forwardRef<HTMLDivElement, TimerIconProps>(
  ({ size, loading, icon: Icon = Clock, className, ...props }, ref) => {
    return (
      <div
        ref={ref}
        className={cn(timerIconVariants({ size, loading }), className)}
        {...props}
      >
        <Icon className="h-full w-full" />
      </div>
    )
  }
)
TimerIcon.displayName = "TimerIcon"

/**
 * Display component for formatted time
 */
export const TimerDisplay = React.forwardRef<HTMLDivElement, TimerDisplayProps>(
  ({ size, time, label, className, ...props }, ref) => {
    return (
      <div
        ref={ref}
        className={cn(timerDisplayVariants({ size }), className)}
        aria-label={label || `Timer: ${time}`}
        {...props}
      >
        {time}
      </div>
    )
  }
)
TimerDisplay.displayName = "TimerDisplay"

export type TimerProps = TimerRootProps &
  UseTimerOptions & {
    /** Time format to display */
    format?: "SS.MS" | "MM:SS" | "HH:MM:SS"
  } & React.HTMLAttributes<HTMLDivElement>

export const Timer = React.forwardRef<HTMLDivElement, TimerProps>(
  (
    {
      loading = false,
      onTick,
      resetOnLoadingChange = true,
      format = "SS.MS",
      variant,
      size,
      className,
      ...props
    },
    ref
  ) => {
    const { formattedTime } = useTimer({
      loading,
      onTick,
      resetOnLoadingChange,
      format,
    })

    return (
      <TimerRoot
        ref={ref}
        variant={variant}
        size={size}
        loading={loading}
        className={className}
        {...props}
      >
        <TimerIcon size={size} loading={loading} />
        <TimerDisplay size={size} time={formattedTime.display} />
      </TimerRoot>
    )
  }
)
Timer.displayName = "Timer"

/**
 * Hook for managing timer state and formatting
 */
export function useTimer({
  loading = false,
  onTick,
  resetOnLoadingChange = true,
  format = "SS.MS",
}: UseTimerOptions = {}): UseTimerReturn {
  const [elapsedTime, setElapsedTime] = useState(0)
  const [milliseconds, setMilliseconds] = useState(0)
  const [isRunning, setIsRunning] = useState(false)
  const startTimeRef = useRef<number>(0)
  const rafRef = useRef<number | null>(null)

  const reset = useCallback(() => {
    setElapsedTime(0)
    setMilliseconds(0)
    startTimeRef.current = 0
  }, [])

  const start = useCallback(() => {
    setIsRunning(true)
    startTimeRef.current = performance.now()
  }, [])

  const stop = useCallback(() => {
    setIsRunning(false)
    if (rafRef.current) {
      cancelAnimationFrame(rafRef.current)
    }
  }, [])

  useEffect(() => {
    if (!isRunning) return

    const updateTimer = () => {
      const now = performance.now()
      const elapsed = now - startTimeRef.current

      const newElapsedTime = Math.floor(elapsed / 1000)
      const newMilliseconds = Math.floor(elapsed % 1000)

      setElapsedTime(newElapsedTime)
      setMilliseconds(newMilliseconds)

      rafRef.current = requestAnimationFrame(updateTimer)
    }

    rafRef.current = requestAnimationFrame(updateTimer)

    return () => {
      if (rafRef.current) {
        cancelAnimationFrame(rafRef.current)
      }
    }
  }, [isRunning])

  useEffect(() => {
    if (loading) {
      if (resetOnLoadingChange) {
        reset()
      }
      start()
    } else {
      stop()
    }
  }, [loading, resetOnLoadingChange, reset, start, stop])

  useEffect(() => {
    if (onTick) {
      onTick(elapsedTime, milliseconds)
    }
  }, [elapsedTime, milliseconds, onTick])

  const formatTime = useCallback(
    (totalSeconds: number, ms: number) => {
      const hours = Math.floor(totalSeconds / 3600)
      const minutes = Math.floor((totalSeconds % 3600) / 60)
      const seconds = totalSeconds % 60

      switch (format) {
        case "HH:MM:SS":
          return {
            seconds: seconds.toString().padStart(2, "0"),
            milliseconds: Math.floor(ms / 10)
              .toString()
              .padStart(2, "0"),
            display: `${hours.toString().padStart(2, "0")}:${minutes
              .toString()
              .padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`,
          }
        case "MM:SS":
          const totalMinutes = Math.floor(totalSeconds / 60)
          const remainingSeconds = totalSeconds % 60
          return {
            seconds: remainingSeconds.toString().padStart(2, "0"),
            milliseconds: Math.floor(ms / 10)
              .toString()
              .padStart(2, "0"),
            display: `${totalMinutes
              .toString()
              .padStart(2, "0")}:${remainingSeconds
              .toString()
              .padStart(2, "0")}`,
          }
        case "SS.MS":
        default:
          return {
            seconds: totalSeconds.toString().padStart(2, "0"),
            milliseconds: Math.floor(ms / 10)
              .toString()
              .padStart(2, "0"),
            display: `${totalSeconds.toString().padStart(2, "0")}.${Math.floor(
              ms / 10
            )
              .toString()
              .padStart(2, "0")}`,
          }
      }
    },
    [format]
  )

  const formattedTime = formatTime(elapsedTime, milliseconds)

  return {
    elapsedTime,
    milliseconds,
    formattedTime,
    isRunning,
    reset,
    start,
    stop,
  }
}
```

**Update the import paths to match your project setup.**

## Usage

```tsx
import {
  Timer,
  TimerDisplay,
  TimerIcon,
  TimerRoot,
  useTimer,
} from "@/components/ui/timer";
```

### Basic Timer

```tsx
<Timer loading={true} />
```

### Compound Components

For more flexibility, you can use the individual components:

```tsx
<TimerRoot variant="outline" size="lg">
  <TimerIcon loading={true} />
  <TimerDisplay time="01:23" />
</TimerRoot>
```

### Timer with Custom Format

```tsx
<Timer loading={true} format="MM:SS" variant="outline" size="lg" />
```

### Timer with Callback

```tsx
const [isLoading, setIsLoading] = useState(false);

const handleTick = (seconds: number, milliseconds: number) => {
  console.log(`Elapsed: ${seconds}.${milliseconds}s`);
};

return (
  <div>
    <Timer
      loading={isLoading}
      onTick={handleTick}
      format="SS.MS"
      variant="outline"
    />
    <button onClick={() => setIsLoading(!isLoading)}>
      {isLoading ? "Stop" : "Start"}
    </button>
  </div>
);
```

### Different Formats

```tsx
// Seconds with milliseconds
<Timer loading={true} format="SS.MS" />

// Minutes:Seconds
<Timer loading={true} format="MM:SS" />

// Hours:Minutes:Seconds
<Timer loading={true} format="HH:MM:SS" />
```

## API Reference

### Timer Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `loading` | `boolean` | `false` | Whether the timer is running/loading |
| `onTick` | `(seconds: number, milliseconds: number) => void` | - | Callback called on each timer tick |
| `resetOnLoadingChange` | `boolean` | `true` | Whether to reset timer when loading changes |
| `format` | `"SS.MS" \| "MM:SS" \| "HH:MM:SS"` | `"SS.MS"` | Time display format |
| `variant` | `"default" \| "outline" \| "ghost" \| "destructive"` | `"default"` | Timer variant |
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | Timer size |
| `className` | `string` | - | Additional CSS classes |

### TimerRoot Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `variant` | `"default" \| "outline" \| "ghost" \| "destructive"` | `"default"` | Timer container variant |
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | Timer size |
| `loading` | `boolean` | `false` | Whether the timer is running |
| `className` | `string` | - | Additional CSS classes |

### TimerIcon Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | Icon size |
| `loading` | `boolean` | `false` | Whether to show loading state |
| `icon` | `React.ComponentType<{ className?: string }>` | `Clock` | Custom icon component |
| `className` | `string` | - | Additional CSS classes |

### TimerDisplay Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `time` | `string` | - | Time value to display |
| `label` | `string` | - | Optional label for accessibility |
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | Display size |
| `className` | `string` | - | Additional CSS classes |

### useTimer Hook

The Timer component uses the `useTimer` hook internally, which you can also use directly:

```tsx
const {
  elapsedTime,
  milliseconds,
  formattedTime,
  isRunning,
  reset,
  start,
  stop,
} = useTimer({
  loading: true,
  onTick: (seconds, ms) => console.log(`${seconds}.${ms}s`),
  format: "SS.MS",
});
```

#### useTimer Options

| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `loading` | `boolean` | `false` | Whether the timer is running |
| `onTick` | `(seconds: number, milliseconds: number) => void` | - | Callback called on each timer tick |
| `resetOnLoadingChange` | `boolean` | `true` | Whether to reset timer when loading changes |
| `format` | `"SS.MS" \| "MM:SS" \| "HH:MM:SS"` | `"SS.MS"` | Time display format |

#### useTimer Return

| Property        | Type         | Description                            |
| --------------- | ------------ | -------------------------------------- |
| `elapsedTime`   | `number`     | Elapsed time in seconds                |
| `milliseconds`  | `number`     | Current milliseconds (0-999)           |
| `formattedTime` | `object`     | Formatted time object with display     |
| `isRunning`     | `boolean`    | Whether the timer is currently running |
| `reset`         | `() => void` | Reset the timer to 0                   |
| `start`         | `() => void` | Start the timer                        |
| `stop`          | `() => void` | Stop the timer                         |

## Examples

### Loading State Timer

```tsx
const [isLoading, setIsLoading] = useState(false);

return (
  <div className="flex items-center gap-4">
    <Timer loading={isLoading} variant="outline" size="lg" format="SS.MS" />
    <button onClick={() => setIsLoading(!isLoading)}>
      {isLoading ? "Stop" : "Start"}
    </button>
  </div>
);
```

### Timer with Progress Tracking

```tsx
const [isLoading, setIsLoading] = useState(false);
const [elapsedTime, setElapsedTime] = useState(0);

const handleTick = (seconds: number, milliseconds: number) => {
  setElapsedTime(seconds);
  // Update progress bar, send analytics, etc.
};

return (
  <div className="space-y-4">
    <Timer
      loading={isLoading}
      onTick={handleTick}
      variant="ghost"
      size="lg"
      format="MM:SS"
    />
    <div className="text-sm text-muted-foreground">
      Elapsed: {elapsedTime} seconds
    </div>
    <button onClick={() => setIsLoading(!isLoading)}>
      {isLoading ? "Stop" : "Start"}
    </button>
  </div>
);
```

### Different Time Formats

```tsx
// For short durations (seconds with milliseconds)
<Timer loading={true} format="SS.MS" variant="default" />

// For medium durations (minutes:seconds)
<Timer loading={true} format="MM:SS" variant="outline" />

// For long durations (hours:minutes:seconds)
<Timer loading={true} format="HH:MM:SS" variant="ghost" />
```

### Custom Timer with Compound Components

```tsx
function CustomTimer() {
  const { formattedTime, isRunning, start, stop, reset } = useTimer({
    format: "MM:SS",
  });

  return (
    <div className="flex items-center gap-4">
      <TimerRoot variant="outline" size="lg">
        <TimerIcon loading={isRunning} />
        <TimerDisplay time={formattedTime.display} />
      </TimerRoot>

      <div className="flex gap-2">
        <button onClick={isRunning ? stop : start}>
          {isRunning ? (
            <Pause className="w-4 h-4" />
          ) : (
            <Play className="w-4 h-4" />
          )}
        </button>
        <button onClick={reset}>
          <RotateCcw className="w-4 h-4" />
        </button>
      </div>
    </div>
  );
}
```

### Custom Icon

```tsx
<TimerRoot variant="destructive" size="lg">
  <TimerIcon loading={true} icon={Stopwatch} />
  <TimerDisplay time="02:15" />
</TimerRoot>
```

### Using the Hook Directly

```tsx
function CustomTimer() {
  const { elapsedTime, formattedTime, isRunning, start, stop, reset } =
    useTimer({
      loading: false,
      format: "MM:SS",
    });

  return (
    <div className="space-y-4">
      <div className="text-2xl font-mono">{formattedTime.display}</div>
      <div className="flex gap-2">
        <button onClick={isRunning ? stop : start}>
          {isRunning ? "Stop" : "Start"}
        </button>
        <button onClick={reset}>Reset</button>
      </div>
    </div>
  );
}
```
