{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "announcement-banner",
  "title": "Announcement Banner",
  "type": "registry:block",
  "description": "Interactive release, security, and maintenance broadcast center with rotating live ticker, CLI copy button, and floating pill / top-bar modes.",
  "categories": [
    "marketing"
  ],
  "deprecated": true,
  "replacedBy": "announcement",
  "files": [
    {
      "path": "packages/elements-lit/src/blocks/announcement-banner/announcement-banner.ts",
      "content": "import { LitElement, css, html, nothing } from 'lit'\nimport { ArrowRight, ChevronLeft, ChevronRight, Megaphone, Terminal, X } from 'lucide'\nimport { cn } from '../../lib/utils'\nimport { tailwind } from '../../lib/styles'\nimport { ThemeController } from '../../lib/theme'\nimport { icon } from '../../lib/icon'\n// Side-effect import: registers <uip-badge> for standalone use (consumers\n// who install only this block still get a working badge).\nimport '../../components/badge/badge'\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\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\n/**\n * <uip-announcement-banner> — the registry AnnouncementBanner block as a web component.\n *\n * Class strings are React's AnnouncementBanner verbatim. React state\n * (useState) becomes @state fields; the autoplay interval runs from\n * `connectedCallback` / `updated` and is cleared in\n * `disconnectedCallback`. Dismissal is internal like React (no callbacks);\n * `reset()` restores a dismissed banner for demos.\n */\nexport class UipAnnouncementBanner extends LitElement {\n  static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true }\n  // :host display can't be a utility class (the host has no template of its own).\n  static styles = [tailwind, css`:host { display: block; }`]\n\n  static properties = {\n    variant: { reflect: true },\n    autoplay: { type: Boolean },\n    currentIndex: { state: true },\n    dismissed: { state: true },\n    isPaused: { state: true },\n    copiedCli: { state: true },\n  }\n\n  variant: 'floating-pill' | 'top-bar' | 'interactive-ticker' = 'interactive-ticker'\n  autoplay = true\n  private currentIndex = 0\n  private dismissed = false\n  private isPaused = false\n  private copiedCli = false\n  private timer?: number\n  private copyTimer?: number\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'announcement-banner')\n    this.setAttribute('data-variant', this.variant)\n    this.syncTimer()\n  }\n\n  disconnectedCallback() {\n    super.disconnectedCallback()\n    this.stopTimer()\n    if (this.copyTimer) window.clearTimeout(this.copyTimer)\n  }\n\n  protected updated(changed: Map<string, unknown>) {\n    if (changed.has('variant')) this.setAttribute('data-variant', this.variant)\n    if (changed.has('autoplay') || changed.has('isPaused')) this.syncTimer()\n  }\n\n  private syncTimer() {\n    this.stopTimer()\n    if (!this.autoplay || this.isPaused) return\n    this.timer = window.setInterval(() => {\n      this.currentIndex = (this.currentIndex + 1) % announcements.length\n    }, 6000)\n  }\n\n  private stopTimer() {\n    if (this.timer) window.clearTimeout(this.timer)\n    this.timer = undefined\n  }\n\n  /** Restores a dismissed banner (demo affordance; React remounts via `key`). */\n  reset() {\n    this.dismissed = false\n    this.currentIndex = 0\n  }\n\n  private nextAnnouncement() {\n    this.currentIndex = (this.currentIndex + 1) % announcements.length\n  }\n\n  private prevAnnouncement() {\n    this.currentIndex = (this.currentIndex - 1 + announcements.length) % announcements.length\n  }\n\n  private copyCliSnippet(snippet?: string) {\n    if (!snippet) return\n    try {\n      navigator.clipboard?.writeText(snippet)?.catch(() => {})\n    } catch {\n      // Clipboard unavailable (insecure context): still flash the label.\n    }\n    this.copiedCli = true\n    if (this.copyTimer) window.clearTimeout(this.copyTimer)\n    this.copyTimer = window.setTimeout(() => {\n      this.copiedCli = false\n    }, 2000)\n  }\n\n  render() {\n    if (this.dismissed) return nothing\n    const current = announcements[this.currentIndex]\n    return html`\n      <div\n        data-slot=\"announcement-banner\"\n        data-variant=${this.variant}\n        role=\"region\"\n        aria-label=\"Announcement\"\n        class=${cn('relative z-50 transition-colors duration-200')}\n        @mouseenter=${() => {\n          this.isPaused = true\n        }}\n        @mouseleave=${() => {\n          this.isPaused = false\n        }}\n      >\n        ${this.variant === 'interactive-ticker'\n          ? html`<div\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                <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 animate-pulse rounded-full\"></span>\n                    <span class=\"text-foreground font-semibold\">Live Dispatch</span>\n                  </div>\n\n                  <uip-badge\n                    variant=\"secondary\"\n                    class=${cn(\n                      'px-2 py-0.5 font-mono text-xs',\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                    >${current.badgeText}</uip-badge\n                  >\n                </div>\n\n                <div class=\"flex flex-1 items-center justify-center gap-2 truncate\">\n                  <p class=\"text-foreground truncate font-medium\">${current.title}</p>\n                  ${current.actionUrl\n                    ? html`<a\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} ${icon(ArrowRight, 'arrow-right', 'size-3')}\n                      </a>`\n                    : nothing}\n                </div>\n\n                <div class=\"flex shrink-0 items-center gap-2\">\n                  ${current.cliSnippet\n                    ? html`<button\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=${() => this.copyCliSnippet(current.cliSnippet)}\n                      >\n                        ${icon(Terminal, 'terminal', 'text-primary size-3')}\n                        <span>${this.copiedCli ? 'Copied CLI!' : 'Copy CLI'}</span>\n                      </button>`\n                    : nothing}\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=${this.prevAnnouncement}\n                    >\n                      ${icon(ChevronLeft, 'chevron-left', 'size-3.5')}\n                    </button>\n                    <span class=\"text-muted-foreground px-1.5 font-mono text-xs\">\n                      ${this.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=${this.nextAnnouncement}\n                    >\n                      ${icon(ChevronRight, 'chevron-right', '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=${() => {\n                      this.dismissed = true\n                    }}\n                  >\n                    ${icon(X, 'x', 'size-3.5')}\n                  </button>\n                </div>\n              </div>\n            </div>`\n          : nothing}\n        ${this.variant === 'floating-pill'\n          ? html`<div 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                <uip-badge variant=\"secondary\" class=\"bg-primary/10 text-primary border-primary/20 px-2 py-0.5 font-mono text-xs\"\n                  >${current.badgeText}</uip-badge\n                >\n                <p class=\"text-foreground max-w-sm truncate font-medium\">${current.title}</p>\n                ${current.actionUrl\n                  ? html`<a\n                      href=${current.actionUrl}\n                      class=\"text-primary inline-flex shrink-0 items-center gap-1 font-semibold hover:underline\"\n                    >\n                      ${current.actionLabel} ${icon(ArrowRight, 'arrow-right', 'size-3')}\n                    </a>`\n                  : nothing}\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=${() => {\n                    this.dismissed = true\n                  }}\n                >\n                  ${icon(X, 'x', 'size-3.5')}\n                </button>\n              </div>\n            </div>`\n          : nothing}\n        ${this.variant === 'top-bar'\n          ? html`<div\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                  ${icon(Megaphone, 'megaphone', '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                  ${current.actionUrl\n                    ? html`<a\n                        href=${current.actionUrl}\n                        class=\"inline-flex items-center gap-1 font-semibold underline-offset-4 hover:underline\"\n                      >\n                        ${current.actionLabel} ${icon(ArrowRight, 'arrow-right', 'size-3')}\n                      </a>`\n                    : nothing}\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=${() => {\n                      this.dismissed = true\n                    }}\n                  >\n                    ${icon(X, 'x', 'size-3.5')}\n                  </button>\n                </div>\n              </div>\n            </div>`\n          : nothing}\n      </div>\n    `\n  }\n}\n\ncustomElements.get('uip-announcement-banner') ||\n  customElements.define('uip-announcement-banner', UipAnnouncementBanner)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-announcement-banner': UipAnnouncementBanner\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/blocks/announcement-banner/announcement-banner.ts"
    }
  ],
  "dependencies": [
    "lit",
    "lucide"
  ],
  "registryDependencies": [
    "https://uipkge.dev/r/lit/badge.json",
    "https://uipkge.dev/r/lit/lit-core.json"
  ]
}
