{
  "$schema": "https://shadcn-vue.com/schema/registry-item.json",
  "name": "announcement",
  "title": "Announcement",
  "type": "registry:block",
  "files": [
    {
      "path": "packages/registry-vue/blocks/announcement/AnnouncementBanner.vue",
      "content": "<script setup lang=\"ts\">\nimport { computed, onMounted, onUnmounted, ref } from 'vue'\nimport type { HTMLAttributes } from 'vue'\nimport { ArrowRight, ChevronLeft, ChevronRight, Megaphone, Terminal, X } from 'lucide-vue-next'\nimport { Badge } from '@/components/ui/badge'\nimport { cn } from '@/lib/utils'\n\ninterface AnnouncementItem {\n  id: string\n  category: 'release' | 'security' | 'maintenance' | 'feature'\n  badgeText: string\n  title: string\n  actionLabel: string\n  actionUrl?: string\n  cliSnippet?: string\n}\n\ninterface Props {\n  variant?: 'floating-pill' | 'top-bar' | 'interactive-ticker'\n  autoplay?: boolean\n  class?: HTMLAttributes['class']\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n  variant: 'interactive-ticker',\n  autoplay: true,\n})\n\nconst announcements: AnnouncementItem[] = [\n  {\n    id: 'ann-1',\n    category: 'release',\n    badgeText: 'v2.4.0 Release',\n    title: 'Tailwind CSS v4 OKLCH tokens & 281+ production workbenches are live.',\n    actionLabel: 'Read Changelog',\n    actionUrl: 'https://uipkge.dev/changelog',\n    cliSnippet: 'npx shadcn-vue@latest add https://uipkge.dev/r/vue/init.json',\n  },\n  {\n    id: 'ann-2',\n    category: 'feature',\n    badgeText: 'Dual-Framework',\n    title: '100% Vue 3.5 & React 19 component parity achieved with zero runtime CSS.',\n    actionLabel: 'Explore Parity Matrix',\n    actionUrl: 'https://uipkge.dev/components',\n    cliSnippet: 'npx shadcn@latest add https://uipkge.dev/r/react/init.json',\n  },\n  {\n    id: 'ann-3',\n    category: 'security',\n    badgeText: 'Security Notice',\n    title: 'Upstream Reka UI & Radix dependency security patches verified and updated.',\n    actionLabel: 'Security Advisory',\n    actionUrl: 'https://uipkge.dev/security',\n  },\n]\n\nconst currentIndex = ref(0)\nconst dismissed = ref(false)\nconst isPaused = ref(false)\nconst copiedCli = ref(false)\n\nlet timer: ReturnType<typeof setInterval> | undefined\n\nfunction startAutoplay() {\n  if (!props.autoplay) return\n  stopAutoplay()\n  timer = setInterval(() => {\n    if (!isPaused.value) {\n      nextAnnouncement()\n    }\n  }, 6000)\n}\n\nfunction stopAutoplay() {\n  if (timer) clearInterval(timer)\n}\n\nonMounted(() => {\n  startAutoplay()\n})\n\nonUnmounted(() => {\n  stopAutoplay()\n})\n\nfunction nextAnnouncement() {\n  currentIndex.value = (currentIndex.value + 1) % announcements.length\n}\n\nfunction prevAnnouncement() {\n  currentIndex.value = (currentIndex.value - 1 + announcements.length) % announcements.length\n}\n\nfunction copyCliSnippet(snippet?: string) {\n  if (!snippet) return\n  navigator.clipboard.writeText(snippet)\n  copiedCli.value = true\n  setTimeout(() => (copiedCli.value = false), 2000)\n}\n\nconst current = computed(() => announcements[currentIndex.value])\n</script>\n\n<template>\n  <div\n    v-if=\"!dismissed\"\n    data-slot=\"announcement-banner\"\n    :data-variant=\"props.variant\"\n    role=\"region\"\n    aria-label=\"Announcement\"\n    :class=\"cn('relative z-50 transition-colors duration-200', props.class)\"\n    @mouseenter=\"isPaused = true\"\n    @mouseleave=\"isPaused = false\"\n  >\n    <!-- 1. Interactive Ticker Mode (Default) -->\n    <div\n      v-if=\"props.variant === 'interactive-ticker'\"\n      class=\"bg-muted/70 border-border text-foreground flex items-center justify-between gap-3 overflow-hidden border-b px-4 py-2 text-xs shadow-xs backdrop-blur-md\"\n    >\n      <div class=\"mx-auto flex w-full max-w-6xl items-center justify-between gap-4\">\n        <!-- Left: Status Beacon & Item Indicator -->\n        <div class=\"flex shrink-0 items-center gap-3\">\n          <div class=\"text-muted-foreground flex items-center gap-1.5 font-mono text-xs\">\n            <span class=\"bg-primary size-2 rounded-full\" />\n            <span class=\"text-foreground font-semibold\">Live Dispatch</span>\n          </div>\n\n          <Badge\n            variant=\"secondary\"\n            class=\"px-2 py-0.5 font-mono text-xs\"\n            :class=\"[\n              current.category === 'release' && 'bg-primary/10 text-primary border-primary/20',\n              current.category === 'security' && 'border-warning/20 bg-warning/10 text-warning',\n              current.category === 'feature' && 'border-success/20 bg-success/10 text-success',\n            ]\"\n          >\n            {{ current.badgeText }}\n          </Badge>\n        </div>\n\n        <!-- Center: Dynamic Headline & Action Link -->\n        <div class=\"flex flex-1 items-center justify-center gap-2 truncate\">\n          <p class=\"text-foreground truncate font-medium\">\n            {{ current.title }}\n          </p>\n          <a\n            v-if=\"current.actionUrl\"\n            :href=\"current.actionUrl\"\n            class=\"text-primary hidden shrink-0 items-center gap-1 font-semibold hover:underline sm:inline-flex\"\n          >\n            {{ current.actionLabel }} <ArrowRight class=\"size-3\" />\n          </a>\n        </div>\n\n        <!-- Right: Ticker Navigation & CLI / Dismiss -->\n        <div class=\"flex shrink-0 items-center gap-2\">\n          <button\n            v-if=\"current.cliSnippet\"\n            type=\"button\"\n            class=\"bg-background border-border text-muted-foreground hover:text-foreground hidden items-center gap-1.5 rounded border px-2 py-0.5 font-mono text-xs transition-colors md:inline-flex\"\n            @click=\"copyCliSnippet(current.cliSnippet)\"\n          >\n            <Terminal class=\"text-primary size-3\" />\n            <span>{{ copiedCli ? 'Copied CLI!' : 'Copy CLI' }}</span>\n          </button>\n\n          <div class=\"border-border bg-background flex items-center overflow-hidden rounded-md border\">\n            <button\n              type=\"button\"\n              class=\"hover:bg-muted text-muted-foreground hover:text-foreground p-1 transition-colors\"\n              aria-label=\"Previous announcement\"\n              @click=\"prevAnnouncement\"\n            >\n              <ChevronLeft class=\"size-3.5\" />\n            </button>\n            <span class=\"text-muted-foreground px-1.5 font-mono text-xs\">\n              {{ currentIndex + 1 }}/{{ announcements.length }}\n            </span>\n            <button\n              type=\"button\"\n              class=\"hover:bg-muted text-muted-foreground hover:text-foreground p-1 transition-colors\"\n              aria-label=\"Next announcement\"\n              @click=\"nextAnnouncement\"\n            >\n              <ChevronRight class=\"size-3.5\" />\n            </button>\n          </div>\n\n          <button\n            type=\"button\"\n            aria-label=\"Dismiss banner\"\n            class=\"text-muted-foreground hover:text-foreground hover:bg-muted rounded-md p-1 transition-colors\"\n            @click=\"dismissed = true\"\n          >\n            <X class=\"size-3.5\" />\n          </button>\n        </div>\n      </div>\n    </div>\n\n    <!-- 2. Floating Pill Mode -->\n    <div v-else-if=\"props.variant === 'floating-pill'\" class=\"mx-auto w-fit max-w-2xl px-4 py-2\">\n      <div\n        class=\"bg-card/90 border-border text-foreground hover:border-primary/40 flex items-center gap-3 rounded-full border py-1.5 pr-2 pl-3 text-xs shadow-md backdrop-blur-md transition-colors\"\n      >\n        <Badge variant=\"secondary\" class=\"bg-primary/10 text-primary border-primary/20 px-2 py-0.5 font-mono text-xs\">\n          {{ current.badgeText }}\n        </Badge>\n        <p class=\"text-foreground max-w-sm truncate font-medium\">\n          {{ current.title }}\n        </p>\n        <a\n          v-if=\"current.actionUrl\"\n          :href=\"current.actionUrl\"\n          class=\"text-primary inline-flex shrink-0 items-center gap-1 font-semibold hover:underline\"\n        >\n          {{ current.actionLabel }} <ArrowRight class=\"size-3\" />\n        </a>\n        <button\n          type=\"button\"\n          aria-label=\"Dismiss announcement\"\n          class=\"text-muted-foreground hover:text-foreground hover:bg-muted rounded-full p-1 transition-colors\"\n          @click=\"dismissed = true\"\n        >\n          <X class=\"size-3.5\" />\n        </button>\n      </div>\n    </div>\n\n    <!-- 3. Top Solid Bar Mode -->\n    <div\n      v-else\n      class=\"bg-primary text-primary-foreground flex items-center justify-between gap-3 px-4 py-2 text-xs shadow-xs\"\n    >\n      <div class=\"mx-auto flex w-full max-w-6xl items-center justify-between gap-4\">\n        <div class=\"flex items-center gap-2 truncate\">\n          <Megaphone class=\"size-4 shrink-0\" />\n          <span class=\"font-mono font-bold\">[{{ current.badgeText }}]</span>\n          <span class=\"truncate\">{{ current.title }}</span>\n        </div>\n        <div class=\"flex shrink-0 items-center gap-3\">\n          <a\n            v-if=\"current.actionUrl\"\n            :href=\"current.actionUrl\"\n            class=\"inline-flex items-center gap-1 font-semibold underline-offset-4 hover:underline\"\n          >\n            {{ current.actionLabel }} <ArrowRight class=\"size-3\" />\n          </a>\n          <button\n            type=\"button\"\n            aria-label=\"Dismiss banner\"\n            class=\"text-primary-foreground/80 hover:text-primary-foreground hover:bg-primary-foreground/15 rounded p-1 transition-colors\"\n            @click=\"dismissed = true\"\n          >\n            <X class=\"size-3.5\" />\n          </button>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n",
      "type": "registry:block",
      "target": "~/app/components/blocks/announcement/AnnouncementBanner.vue"
    },
    {
      "path": "packages/registry-vue/blocks/announcement/AnnouncementBannerSplit.vue",
      "content": "<script setup lang=\"ts\">\nimport { ArrowRight, CircleAlert, Info } from 'lucide-vue-next'\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\nimport { Separator } from '@/components/ui/separator'\n\nwithDefaults(defineProps<{ tone?: 'neutral' | 'attention' }>(), { tone: 'neutral' })\n</script>\n\n<template>\n  <section\n    data-slot=\"announcement-banner-split\"\n    class=\"border-b\"\n    :class=\"tone === 'attention' ? 'border-border bg-muted' : 'border-border bg-card'\"\n  >\n    <div class=\"mx-auto flex max-w-6xl flex-col gap-4 px-6 py-4 sm:flex-row sm:items-center sm:gap-8\">\n      <!-- Message column carries the classification; the action column stays\n           the same width whatever the message length, so a stack of these\n           banners aligns down the page. -->\n      <div class=\"flex min-w-0 flex-1 items-start gap-3\">\n        <component\n          :is=\"tone === 'attention' ? CircleAlert : Info\"\n          class=\"mt-0.5 size-4 shrink-0\"\n          :class=\"tone === 'attention' ? 'text-destructive' : 'text-muted-foreground'\"\n          aria-hidden=\"true\"\n        />\n        <div class=\"min-w-0\">\n          <div class=\"flex flex-wrap items-center gap-2\">\n            <Badge :variant=\"tone === 'attention' ? 'outline' : 'secondary'\">\n              {{ tone === 'attention' ? 'Scheduled maintenance' : 'Release 2.4.0' }}\n            </Badge>\n            <span class=\"text-muted-foreground font-mono text-xs\">Aug 14, 2026 · 02:00–04:00 UTC</span>\n          </div>\n          <p class=\"mt-1.5 text-sm leading-relaxed\">\n            <template v-if=\"tone === 'attention'\">\n              Query serving stays online. Scheduled materialisation pauses for the window and resumes automatically.\n            </template>\n            <template v-else>\n              Command palette, table virtualisation past 10k rows, and AA-contrast sidebar badges in dark mode.\n            </template>\n          </p>\n        </div>\n      </div>\n\n      <Separator orientation=\"vertical\" class=\"hidden h-10 sm:block\" />\n\n      <div class=\"flex shrink-0 items-center gap-2\">\n        <Button variant=\"outline\" size=\"sm\">\n          {{ tone === 'attention' ? 'Status page' : 'Full changelog' }}\n          <ArrowRight class=\"ml-1.5 size-3.5\" aria-hidden=\"true\" />\n        </Button>\n        <Button variant=\"ghost\" size=\"sm\">Subscribe</Button>\n      </div>\n    </div>\n  </section>\n</template>\n",
      "type": "registry:block",
      "target": "~/app/components/blocks/announcement/AnnouncementBannerSplit.vue"
    },
    {
      "path": "packages/registry-vue/blocks/announcement/AnnouncementCornerToast.vue",
      "content": "<script setup lang=\"ts\">\nimport { onBeforeUnmount, onMounted, ref } from 'vue'\nimport { ArrowRight, Bell, X } from 'lucide-vue-next'\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\nimport { Card, CardContent } from '@/components/ui/card'\nimport { Separator } from '@/components/ui/separator'\n\nconst props = withDefaults(\n  defineProps<{\n    /** Milliseconds before the card slides in. */\n    delay?: number\n    /** sessionStorage key holding the dismissal. */\n    storageKey?: string\n  }>(),\n  {\n    delay: 1200,\n    storageKey: 'uipkge:corner-announcement-dismissed',\n  },\n)\n\n// Hidden on the server and on first paint; the delay timer is what reveals it.\nconst visible = ref(false)\nlet timer: ReturnType<typeof setTimeout> | undefined\n\nfunction dismiss() {\n  visible.value = false\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\nfunction wasDismissed() {\n  try {\n    return window.sessionStorage.getItem(props.storageKey) === '1'\n  } catch {\n    return false\n  }\n}\n\nonMounted(() => {\n  if (!wasDismissed()) timer = setTimeout(() => (visible.value = true), props.delay)\n})\n\nonBeforeUnmount(() => clearTimeout(timer))\n</script>\n\n<template>\n  <Transition\n    enter-active-class=\"transition duration-300 ease-out\"\n    enter-from-class=\"translate-y-4 opacity-0\"\n    leave-active-class=\"transition duration-200 ease-in\"\n    leave-to-class=\"translate-y-4 opacity-0\"\n  >\n    <div\n      v-show=\"visible\"\n      data-slot=\"announcement-corner-toast\"\n      class=\"fixed right-4 bottom-4 z-50 w-[22rem] max-w-[calc(100vw-2rem)]\"\n      role=\"complementary\"\n      aria-label=\"Product announcement\"\n    >\n      <Card class=\"shadow-lg\">\n        <CardContent class=\"p-4\">\n          <div class=\"flex items-start gap-3\">\n            <span\n              class=\"border-border bg-muted flex size-10 shrink-0 items-center justify-center rounded-lg border\"\n              aria-hidden=\"true\"\n            >\n              <Bell class=\"text-primary size-4\" />\n            </span>\n            <div class=\"min-w-0 flex-1\">\n              <Badge variant=\"secondary\" class=\"mb-1.5\">New</Badge>\n              <p class=\"text-sm font-semibold\">Scoping now follows SCIM groups</p>\n              <p class=\"text-muted-foreground mt-1 text-xs leading-relaxed\">\n                Move someone between teams in your IdP and their dashboards follow within the sync window.\n              </p>\n            </div>\n            <Button\n              variant=\"ghost\"\n              size=\"icon\"\n              class=\"-mt-1 -mr-1 size-7 shrink-0\"\n              aria-label=\"Dismiss announcement\"\n              @click=\"dismiss\"\n            >\n              <X class=\"size-3.5\" aria-hidden=\"true\" />\n            </Button>\n          </div>\n\n          <Separator class=\"my-3\" />\n\n          <div class=\"flex items-center gap-2\">\n            <Button size=\"sm\" class=\"h-7\">\n              Read the changelog\n              <ArrowRight class=\"ml-1 size-3.5\" aria-hidden=\"true\" />\n            </Button>\n            <Button variant=\"ghost\" size=\"sm\" class=\"h-7\" @click=\"dismiss\">Not now</Button>\n          </div>\n        </CardContent>\n      </Card>\n    </div>\n  </Transition>\n</template>\n",
      "type": "registry:block",
      "target": "~/app/components/blocks/announcement/AnnouncementCornerToast.vue"
    },
    {
      "path": "packages/registry-vue/blocks/announcement/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/announcement/AnnouncementCountdownBar.vue"
    },
    {
      "path": "packages/registry-vue/blocks/announcement/AnnouncementRotatingStrip.vue",
      "content": "<script setup lang=\"ts\">\nimport { onBeforeUnmount, onMounted, ref } from 'vue'\nimport { ArrowRight } from 'lucide-vue-next'\nimport { Badge } from '@/components/ui/badge'\nimport { Button } from '@/components/ui/button'\n\nconst props = withDefaults(defineProps<{ interval?: number }>(), { interval: 6000 })\n\nconst messages = [\n  { label: 'New', text: 'Row-level scoping now evaluates against SCIM groups.', cta: 'Changelog' },\n  { label: 'Event', text: 'Live walkthrough of the five-week rollout, every Thursday.', cta: 'Save a seat' },\n  { label: 'Docs', text: 'The reconciliation checklist for your first close is published.', cta: 'Read it' },\n]\n\nconst index = ref(0)\nconst paused = ref(false)\nlet timer: ReturnType<typeof setInterval> | undefined\n\nfunction advance() {\n  index.value = (index.value + 1) % messages.length\n}\n\nfunction start() {\n  stop()\n  timer = setInterval(() => {\n    if (!paused.value) advance()\n  }, props.interval)\n}\n\nfunction stop() {\n  clearInterval(timer)\n  timer = undefined\n}\n\nonMounted(start)\nonBeforeUnmount(stop)\n</script>\n\n<template>\n  <div\n    data-slot=\"announcement-rotating-strip\"\n    class=\"border-border bg-muted/60 border-b backdrop-blur\"\n    @mouseenter=\"paused = true\"\n    @mouseleave=\"paused = false\"\n    @focusin=\"paused = true\"\n    @focusout=\"paused = false\"\n  >\n    <div class=\"mx-auto flex h-11 max-w-6xl items-center gap-3 px-6 text-sm\">\n      <!-- aria-live announces the rotation to screen readers without stealing focus. -->\n      <div class=\"flex min-w-0 flex-1 items-center gap-3\" aria-live=\"polite\" aria-atomic=\"true\">\n        <Badge variant=\"secondary\" class=\"shrink-0\">{{ messages[index].label }}</Badge>\n        <p class=\"min-w-0 truncate\">{{ messages[index].text }}</p>\n        <Button variant=\"link\" size=\"sm\" class=\"hidden h-auto shrink-0 p-0 text-xs sm:inline-flex\">\n          {{ messages[index].cta }}\n          <ArrowRight class=\"ml-1 size-3\" aria-hidden=\"true\" />\n        </Button>\n      </div>\n\n      <div class=\"flex shrink-0 items-center gap-1.5\">\n        <button\n          v-for=\"(message, i) in messages\"\n          :key=\"message.text\"\n          type=\"button\"\n          class=\"focus-visible:ring-ring size-1.5 rounded-full transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none\"\n          :class=\"i === index ? 'bg-foreground' : 'bg-muted-foreground/40 hover:bg-muted-foreground'\"\n          :aria-label=\"`Show announcement ${i + 1} of ${messages.length}`\"\n          :aria-current=\"i === index ? 'true' : undefined\"\n          @click=\"index = i\"\n        />\n      </div>\n    </div>\n  </div>\n</template>\n",
      "type": "registry:block",
      "target": "~/app/components/blocks/announcement/AnnouncementRotatingStrip.vue"
    },
    {
      "path": "packages/registry-vue/blocks/announcement/BannerAnnouncementSticky.vue",
      "content": "<script setup lang=\"ts\">\nimport { ref } from 'vue'\nimport { ArrowRight, Megaphone, X } from 'lucide-vue-next'\nimport { Badge } from '@/components/ui/badge'\n\nconst isVisible = ref(true)\n\nfunction dismissBanner() {\n  isVisible.value = false\n}\n</script>\n\n<template>\n  <aside\n    v-if=\"isVisible\"\n    data-slot=\"banner-announcement-sticky\"\n    class=\"border-border bg-card/90 relative z-50 border-b px-4 py-2.5 backdrop-blur-md transition-colors sm:px-6\"\n  >\n    <div class=\"mx-auto flex max-w-7xl items-center justify-between gap-4 font-mono text-xs\">\n      <!-- Left / Center Content -->\n      <div class=\"flex min-w-0 flex-1 items-center justify-center gap-3 sm:justify-start\">\n        <Badge\n          variant=\"secondary\"\n          class=\"bg-primary/10 text-primary border-primary/20 shrink-0 px-2 py-0.5 text-xs font-bold tracking-wider uppercase\"\n        >\n          v2.4.0 Release\n        </Badge>\n\n        <p class=\"text-foreground flex items-center gap-1.5 truncate\">\n          <Megaphone class=\"text-muted-foreground hidden size-3 shrink-0 sm:inline\" />\n          <span>Announcing 450+ Marketing &amp; SaaS Blocks with Tailwind v4 OKLCH token engine.</span>\n        </p>\n\n        <a href=\"#\" class=\"text-primary inline-flex shrink-0 items-center gap-1 font-bold hover:underline\">\n          <span>Read Release Notes</span>\n          <ArrowRight class=\"size-3\" />\n        </a>\n      </div>\n\n      <!-- Dismiss Trigger Button -->\n      <button\n        type=\"button\"\n        class=\"hover:bg-muted text-muted-foreground hover:text-foreground flex size-6 shrink-0 items-center justify-center rounded-md transition-colors\"\n        aria-label=\"Dismiss banner\"\n        @click=\"dismissBanner\"\n      >\n        <X class=\"size-3.5\" />\n      </button>\n    </div>\n  </aside>\n</template>\n",
      "type": "registry:block",
      "target": "~/app/components/blocks/announcement/BannerAnnouncementSticky.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/card.json",
    "https://uipkge.dev/r/vue/separator.json"
  ],
  "description": "Announcement band set covering six placements: a dismissible top banner in solid, soft, and gradient tones, a split release-notes band, a rotating multi-message strip, a session countdown bar, a delayed corner card, and a sticky version-pill banner.",
  "categories": [
    "marketing"
  ]
}