{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "announcement-countdown-bar",
  "title": "Announcement — Countdown Bar",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-vue/blocks/announcement-countdown-bar/AnnouncementCountdownBar.vue",
      "content": "<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, onMounted, ref } from 'vue'\nimport { ArrowRight, Timer, X } from 'lucide-vue-next'\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\nimport { Separator } from '@/components/ui/separator'\n\nconst props = withDefaults(\n  defineProps<{\n    /** ISO timestamp the countdown runs to. */\n    deadline?: string\n    /** sessionStorage key holding the dismissal. */\n    storageKey?: string\n  }>(),\n  {\n    deadline: '',\n    storageKey: 'uipkge:countdown-dismissed',\n  },\n)\n\nconst dismissed = ref(false)\n// Null until mounted: the server has no clock the client agrees with, so the\n// digits only appear once the timer is running.\nconst remaining = ref<number | null>(null)\nlet timer: ReturnType<typeof setInterval> | undefined\n\nconst target = computed(() => {\n  const parsed = props.deadline ? Date.parse(props.deadline) : Number.NaN\n  // Fall back to 72 hours out so the demo and a mis-set prop still read sensibly.\n  return Number.isNaN(parsed) ? Date.now() + 72 * 60 * 60 * 1000 : parsed\n})\n\nconst parts = computed(() => {\n  const ms = Math.max(remaining.value ?? 0, 0)\n  const total = Math.floor(ms / 1000)\n  return [\n    { label: 'd', value: Math.floor(total / 86400) },\n    { label: 'h', value: Math.floor((total % 86400) / 3600) },\n    { label: 'm', value: Math.floor((total % 3600) / 60) },\n    { label: 's', value: total % 60 },\n  ]\n})\n\nfunction dismiss() {\n  dismissed.value = true\n  try {\n    window.sessionStorage.setItem(props.storageKey, '1')\n  } catch {\n    // Blocked storage: the dismissal still holds for this page view.\n  }\n}\n\nonMounted(() => {\n  try {\n    dismissed.value = window.sessionStorage.getItem(props.storageKey) === '1'\n  } catch {\n    dismissed.value = false\n  }\n  const tick = () => (remaining.value = target.value - Date.now())\n  tick()\n  timer = setInterval(tick, 1000)\n})\n\nonBeforeUnmount(() => clearInterval(timer))\n</script>\n\n<template>\n  <div\n    v-if=\"!dismissed\"\n    data-slot=\"announcement-countdown-bar\"\n    class=\"border-border bg-muted/60 border-b backdrop-blur\"\n  >\n    <div class=\"mx-auto flex min-h-11 max-w-6xl flex-wrap items-center gap-x-4 gap-y-2 px-6 py-2 text-sm\">\n      <Badge variant=\"secondary\" class=\"shrink-0 gap-1.5\">\n        <Timer class=\"size-3\" aria-hidden=\"true\" />\n        Ends soon\n      </Badge>\n\n      <p class=\"min-w-0\">\n        <span class=\"font-medium\">Annual plans are 20% off</span>\n        <span class=\"text-muted-foreground\"> for teams that start before the quarter closes.</span>\n      </p>\n\n      <!-- tabular-nums + fixed-width cells keep the bar from reflowing each second. -->\n      <div v-if=\"remaining !== null\" class=\"text-muted-foreground flex items-center gap-1 font-mono text-xs\">\n        <span v-for=\"part in parts\" :key=\"part.label\" class=\"tabular-nums\">\n          <span class=\"text-foreground inline-block min-w-[2ch] text-right font-semibold\">\n            {{ String(part.value).padStart(2, '0') }} </span\n          >{{ part.label }}\n        </span>\n      </div>\n\n      <div class=\"ml-auto flex shrink-0 items-center gap-1\">\n        <Button variant=\"ghost\" size=\"sm\" class=\"h-7\">\n          Claim it\n          <ArrowRight class=\"ml-1 size-3.5\" aria-hidden=\"true\" />\n        </Button>\n        <Separator orientation=\"vertical\" class=\"hidden h-5 sm:block\" />\n        <Button variant=\"ghost\" size=\"icon\" class=\"size-7\" aria-label=\"Dismiss announcement\" @click=\"dismiss\">\n          <X class=\"size-3.5\" aria-hidden=\"true\" />\n        </Button>\n      </div>\n    </div>\n  </div>\n</template>\n",
      "type": "registry:block",
      "target": "~/app/components/blocks/AnnouncementCountdownBar.vue"
    }
  ],
  "dependencies": [
    "lucide-vue-next"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/vue/badge.json",
    "https://uipkge.dev/r/vue/button.json",
    "https://uipkge.dev/r/vue/separator.json"
  ],
  "description": "Top strip counting down to a deadline in days, hours, minutes and seconds, with the offer line, an inline CTA, and a dismiss control that keeps it closed for the session.",
  "categories": [
    "marketing"
  ],
  "deprecated": true,
  "replacedBy": "announcement"
}