# Sale Countdown Promo Bar

> Sticky promo announcement bar with a live sale countdown ticking days, hours, minutes, and seconds beside a shop the sale button.

A shadcn/ui block from the [@7ovr](https://7ovr.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/7ovr/announcement-4).

- Page, with a live preview: https://shadcn.exchange/r/7ovr/announcement-4
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/7ovr/announcement-4-light-e0b9dc82a19ca199.png
- Registry item: https://7ovr.com/r/announcement-4.json
- Categories: announcement
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://7ovr.com/r/announcement-4.json
```

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

## Dependencies

- npm packages: `@base-ui/react`
- Registry items: `button`

## Notes from the registry

Requires a `base-*` style in components.json (the `shadcn init` default). Legacy `new-york`/`radix-*` styles install Radix primitives, which reject the `render` prop this block uses.

## Files

### `registry/blocks/announcement/4/announcement-block.tsx`

```tsx
"use client"

import { useEffect, useState } from "react"
import { Button } from "@/components/ui/button"
import { IconPlaceholder } from "@/components/icons/icon-placeholder"

const SALE_DURATION_MS =
  2 * 24 * 60 * 60 * 1000 + 11 * 60 * 60 * 1000 + 34 * 60 * 1000 + 12 * 1000

type TimeLeft = {
  days: number
  hours: number
  minutes: number
  seconds: number
}

function getTimeLeft(diffMs: number): TimeLeft {
  const diff = Math.max(0, diffMs)
  const totalSeconds = Math.floor(diff / 1000)
  return {
    days: Math.floor(totalSeconds / 86400),
    hours: Math.floor((totalSeconds % 86400) / 3600),
    minutes: Math.floor((totalSeconds % 3600) / 60),
    seconds: totalSeconds % 60,
  }
}

function pad(value: number): string {
  return value.toString().padStart(2, "0")
}

export default function AnnouncementBlock() {
  const [timeLeft, setTimeLeft] = useState<TimeLeft>(() =>
    getTimeLeft(SALE_DURATION_MS)
  )

  useEffect(() => {
    const deadline = Date.now() + SALE_DURATION_MS
    const interval = setInterval(() => {
      setTimeLeft(getTimeLeft(deadline - Date.now()))
    }, 1000)

    return () => clearInterval(interval)
  }, [])

  return (
    <section className="flex min-h-svh w-full flex-col bg-muted/30 text-foreground">
      <div className="w-full bg-primary text-primary-foreground">
        <div className="mx-auto flex w-full max-w-6xl flex-col items-center gap-3 px-6 py-3 sm:flex-row sm:justify-between">
          <div className="flex flex-col items-center gap-2.5 sm:flex-row sm:gap-3">
            <span className="flex items-center gap-2 text-sm font-semibold">
              <IconPlaceholder
                lucide="Flame"
                tabler="IconFlame"
                hugeicons="FireIcon"
                phosphor="Fire"
                remixicon="RiFireLine"
                className="size-4 shrink-0"
                aria-hidden="true"
              />
              Summer Sale, up to 40% off
            </span>
            <span className="hidden h-4 w-px bg-primary-foreground/30 sm:block" />
            <span className="flex items-center gap-2 text-sm">
              <span className="text-primary-foreground/80">Sale ends in</span>
              <span className="flex items-center gap-1 font-mono text-sm font-semibold tabular-nums">
                <span className="rounded-md bg-primary-foreground/15 px-1.5 py-0.5">
                  {pad(timeLeft.days)}d
                </span>
                <span className="rounded-md bg-primary-foreground/15 px-1.5 py-0.5">
                  {pad(timeLeft.hours)}h
                </span>
                <span className="rounded-md bg-primary-foreground/15 px-1.5 py-0.5">
                  {pad(timeLeft.minutes)}m
                </span>
                <span className="rounded-md bg-primary-foreground/15 px-1.5 py-0.5">
                  {pad(timeLeft.seconds)}s
                </span>
              </span>
            </span>
          </div>
          <Button
            size="sm"
            variant="secondary"
            render={<a href="#" />}
            nativeButton={false}
            className="w-full sm:w-auto"
          >
            Shop The Sale
            <IconPlaceholder
              lucide="ArrowRight"
              tabler="IconArrowRight"
              hugeicons="ArrowRightIcon"
              phosphor="ArrowRight"
              remixicon="RiArrowRightLine"
              data-icon="inline-end"
              aria-hidden="true"
            />
          </Button>
        </div>
      </div>

      <div
        aria-hidden="true"
        className="mx-auto w-4/5 max-w-5xl flex-1 px-6 py-10"
      >
        <div className="mb-6 flex items-center justify-between">
          <div className="h-6 w-28 rounded-md bg-muted" />
          <div className="hidden items-center gap-3 sm:flex">
            <div className="h-3 w-14 rounded-md bg-muted" />
            <div className="h-3 w-14 rounded-md bg-muted" />
            <div className="h-7 w-16 rounded-md bg-muted" />
          </div>
        </div>

        <div className="space-y-2">
          <div className="h-6 w-1/2 rounded-md bg-muted" />
          <div className="h-3 w-full rounded-md bg-muted" />
          <div className="h-3 w-4/5 rounded-md bg-muted" />
        </div>

        <div className="mt-8 grid gap-3 sm:grid-cols-3">
          {Array.from({ length: 3 }).map((_, index) => (
            <div key={index} className="h-24 rounded-md bg-muted" />
          ))}
        </div>
      </div>
    </section>
  )
}
```
