# Resend Button with Cooldown

> A resend-the-code button that disables itself for a countdown after each press, says how long is left in words, and re-enables itself, with a limit on how many times it can be used. Use this when resending is rate limited.

A shadcn/ui block from the [@tealmac](https://ui.tealmac.com/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/tealmac/resend-cooldown-button).

- Page, with a live preview: https://shadcn.exchange/r/tealmac/resend-cooldown-button
- Screenshot: https://shadcn.exchange/shot/tealmac/resend-cooldown-button/light.png
- Registry item: https://ui.tealmac.com/r/resend-cooldown-button.json
- Listed: 2026-11-01, source last changed 2026-11-01

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/tealmac/resend-cooldown-button.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- Registry items: `button`

## Files

### `registry/patterns/resend-cooldown-button/resend-cooldown-button.tsx`

```tsx
"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"

export interface ResendCooldownButtonProps {
  /** Send again. Reject to show an error and skip the cooldown. */
  onResend?: () => Promise<void>
  /** Wait between sends. Default 30. */
  seconds?: number
  /** Most sends allowed. Default 3. */
  maxSends?: number
}

export function ResendCooldownButton({
  onResend,
  seconds = 30,
  maxSends = 3,
}: ResendCooldownButtonProps) {
  const [left, setLeft] = React.useState(0)
  const [sends, setSends] = React.useState(0)
  const [busy, setBusy] = React.useState(false)
  const [error, setError] = React.useState(false)
  const [announce, setAnnounce] = React.useState("")

  React.useEffect(() => {
    if (left <= 0) return
    const timer = setTimeout(() => {
      setLeft(left - 1)
      // Say the time left at 10, 5 and 0 only, so the countdown does not chatter.
      if ([10, 5].includes(left - 1))
        setAnnounce(`You can resend in ${left - 1} seconds.`)
      if (left - 1 === 0) setAnnounce("You can resend the code now.")
    }, 1000)
    return () => clearTimeout(timer)
  }, [left])

  async function resend() {
    setBusy(true)
    setError(false)
    try {
      await onResend?.()
      setSends((n) => n + 1)
      setLeft(seconds)
      setAnnounce("Code sent.")
    } catch {
      setError(true)
    } finally {
      setBusy(false)
    }
  }

  const exhausted = sends >= maxSends
  return (
    <div className="flex flex-col items-start gap-1">
      <Button
        type="button"
        variant="outline"
        disabled={busy || left > 0 || exhausted}
        onClick={() => void resend()}
      >
        {busy
          ? "Sending..."
          : left > 0
            ? `Resend code in ${left}s`
            : "Resend code"}
      </Button>
      <p className="text-muted-foreground text-xs">
        {exhausted
          ? "You have used all resends. Contact support if the code did not arrive."
          : `${maxSends - sends} ${maxSends - sends === 1 ? "resend" : "resends"} left.`}
      </p>
      {error ? (
        <p role="alert" className="text-destructive text-xs">
          Could not send the code. Try again.
        </p>
      ) : null}
      <span role="status" aria-live="polite" className="sr-only">
        {announce}
      </span>
    </div>
  )
}
```
