{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "announcement-bar",
  "title": "Announcement Bar",
  "description": "A dismissible bar pinned above your header for a message the whole site needs to see: a launch or new release, a promo, sale or free-shipping offer, a scheduled-maintenance or downtime window, an incident or status notice, a beta or early-access note, a cookie or GDPR notice, a plan-expiring or payment-failed warning, or a 'you are viewing the docs for an old version' banner. Give it an id and it remembers the dismissal in localStorage, so a visitor who closes it does not meet it again on the next page or the next visit; bump version (or the id) when the wording changes and it comes back for everyone. It renders nothing until it has mounted, which is the part that is easy to get wrong on your own: reading localStorage while rendering makes the server and the browser disagree and React throws a hydration mismatch, and rendering the bar first and hiding it afterwards flashes a banner the user already dismissed on every single page load. The bar is a labelled landmark region rather than a plain div, so it is reachable by landmark navigation instead of being an unnamed strip of text before the header; the close button has a real accessible name and a focus ring, the leading icon is aria-hidden because it repeats the text, and dismissible={false} drops the button entirely for a banner that must stay put. Two looks — primary is a solid accent bar, default is a muted bar with a bottom border — both from shadcn tokens, so it follows light and dark with the rest of your theme. Takes an action slot for the trailing 'Read more' or 'Upgrade' link and calls onDismiss so you can log it. Official shadcn/ui has nothing that does this: alert is a static box that cannot be closed and remembers nothing, alert-dialog is a modal that blocks the page until answered, and sonner is a toast that floats in and disappears on a timer — none of them is a persistent top-of-page bar, and none of them survives a page load. lucide-react is the only dependency, for the close icon.",
  "dependencies": [
    "lucide-react"
  ],
  "files": [
    {
      "path": "registry/ui/announcement-bar.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { X } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nconst STORAGE_PREFIX = \"pulld-announcement:\"\n\ninterface AnnouncementBarProps\n  extends Omit<React.ComponentPropsWithoutRef<\"div\">, \"id\"> {\n  /**\n   * Stable id for this announcement. The dismissed state is remembered per id\n   * in localStorage, so once a user closes the bar it stays closed on reload.\n   * Change the id — or bump `version` — when the message changes to show it again.\n   */\n  id: string\n  /**\n   * Bump this when the announcement's content changes so previously-dismissed\n   * users see the new message. Any change to the value re-shows the bar.\n   */\n  version?: string | number\n  /** `primary` is a solid accent bar; `default` is a subtle muted bar with a bottom border. */\n  variant?: \"default\" | \"primary\"\n  /** Optional leading icon (decorative; rendered aria-hidden). */\n  icon?: React.ReactNode\n  /** Optional trailing call-to-action, usually a link or button. */\n  action?: React.ReactNode\n  /** Set false for a permanent banner (e.g. maintenance) with no close button. */\n  dismissible?: boolean\n  /** Accessible label for the landmark region. */\n  label?: string\n  /** Called after the user dismisses the bar. */\n  onDismiss?: () => void\n}\n\n/**\n * A dismissible top-of-page announcement / notice bar. Persists its dismissed\n * state to localStorage per `id`, so it does not reappear once closed until the\n * `id` or `version` changes. Renders nothing until mounted so server and first\n * client render agree (localStorage is unavailable on the server) — the bar pops\n * in for first-time visitors but never flashes for users who already closed it.\n */\nexport function AnnouncementBar({\n  id,\n  version,\n  variant = \"default\",\n  icon,\n  action,\n  dismissible = true,\n  label = \"Announcement\",\n  onDismiss,\n  className,\n  children,\n  ...props\n}: AnnouncementBarProps) {\n  const storageKey = STORAGE_PREFIX + id\n  const token = version == null ? \"1\" : String(version)\n\n  const [visible, setVisible] = React.useState(false)\n\n  React.useEffect(() => {\n    if (!dismissible) {\n      setVisible(true)\n      return\n    }\n    let dismissed = false\n    try {\n      dismissed = window.localStorage.getItem(storageKey) === token\n    } catch {\n      // localStorage can throw (private mode, blocked storage) — show the bar.\n    }\n    setVisible(!dismissed)\n  }, [storageKey, token, dismissible])\n\n  function handleDismiss() {\n    setVisible(false)\n    try {\n      window.localStorage.setItem(storageKey, token)\n    } catch {\n      // Ignore write failures; the bar simply reappears on the next load.\n    }\n    onDismiss?.()\n  }\n\n  if (!visible) return null\n\n  return (\n    <div\n      id={id}\n      role=\"region\"\n      aria-label={label}\n      className={cn(\n        \"flex items-center gap-3 px-4 py-2.5 text-sm\",\n        variant === \"primary\"\n          ? \"bg-primary text-primary-foreground\"\n          : \"border-b bg-muted text-foreground\",\n        className\n      )}\n      {...props}\n    >\n      {icon ? (\n        <span className=\"shrink-0\" aria-hidden=\"true\">\n          {icon}\n        </span>\n      ) : null}\n      <div className=\"flex min-w-0 flex-1 flex-wrap items-center gap-x-2 gap-y-0.5\">\n        <span className=\"min-w-0\">{children}</span>\n        {action ? <span className=\"shrink-0 font-medium\">{action}</span> : null}\n      </div>\n      {dismissible ? (\n        <button\n          type=\"button\"\n          onClick={handleDismiss}\n          aria-label=\"Dismiss announcement\"\n          className={cn(\n            \"-mr-1 inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-md transition-colors focus-visible:outline-none focus-visible:ring-2\",\n            variant === \"primary\"\n              ? \"text-primary-foreground/80 hover:bg-primary-foreground/10 hover:text-primary-foreground focus-visible:ring-primary-foreground\"\n              : \"text-muted-foreground hover:bg-foreground/10 hover:text-foreground focus-visible:ring-ring\"\n          )}\n        >\n          <X className=\"h-4 w-4\" aria-hidden=\"true\" />\n        </button>\n      ) : null}\n    </div>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}