{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "banner",
  "title": "Banner",
  "description": "Banner: an announcement bar above the layout, optionally dismissible and with a rainbow variant, that offsets docscn's layouts by its height.",
  "dependencies": [
    "cn",
    "@fuma-translate/react@^1.0.2",
    "lucide-react"
  ],
  "registryDependencies": [
    "button"
  ],
  "files": [
    {
      "path": "registry/base/docs/components/banner.tsx",
      "content": "// Adapted from Fumadocs UI (https://github.com/fuma-nama/fumadocs)\n// Copyright (c) 2023 Fuma, MIT License\n\"use client\";\n\nimport { useTranslations } from \"@fuma-translate/react\";\nimport { cn } from \"cn\";\nimport { X } from \"lucide-react\";\nimport { type HTMLAttributes, useState, useSyncExternalStore } from \"react\";\nimport { Button } from \"@/components/ui/button\";\n\ntype BannerVariant = \"rainbow\" | \"normal\";\n\nconst noop = () => () => {};\n\nexport function Banner({\n  id,\n  variant = \"normal\",\n  changeLayout = true,\n  height = \"3rem\",\n  rainbowColors = [\n    \"rgba(0,149,255,0.56)\",\n    \"rgba(231,77,255,0.77)\",\n    \"rgba(255,0,0,0.73)\",\n    \"rgba(131,255,166,0.66)\",\n  ],\n  ...props\n}: HTMLAttributes<HTMLDivElement> & {\n  /**\n   * @defaultValue 3rem\n   */\n  height?: string;\n\n  /**\n   * @defaultValue 'normal'\n   */\n  variant?: BannerVariant;\n\n  /**\n   * For rainbow variant only, customize the colors\n   */\n  rainbowColors?: string[];\n\n  /**\n   * Offset docscn's layouts (sidebar, navbars and table of contents) by the\n   * banner's height, through the `--docs-banner-height` variable\n   *\n   * @defaultValue true\n   */\n  changeLayout?: boolean;\n}) {\n  const globalKey = id ? `nd-banner-${encodeBase32(id)}` : null;\n  const [closed, setClosed] = useState(false);\n  const t = useTranslations({ note: \"banner\" });\n  // a banner closed on an earlier visit, `false` on the server\n  const dismissed = useSyncExternalStore(\n    noop,\n    () => (globalKey ? localStorage.getItem(globalKey) === \"true\" : false),\n    () => false,\n  );\n\n  function onClose() {\n    setClosed(true);\n    if (globalKey) localStorage.setItem(globalKey, \"true\");\n    if (globalKey) document.documentElement.classList.add(globalKey);\n  }\n\n  if (closed || dismissed) return null;\n\n  return (\n    <div\n      id={id}\n      {...props}\n      className={cn(\n        \"sticky top-0 z-40 flex flex-row items-center justify-center px-4 text-center text-sm font-medium\",\n        variant === \"normal\" && \"bg-secondary text-secondary-foreground\",\n        variant === \"rainbow\" && \"bg-background\",\n        props.className,\n      )}\n      style={{\n        height,\n      }}\n    >\n      {changeLayout ? (\n        <style>\n          {globalKey\n            ? `:root:not(.${globalKey}) { --docs-banner-height: ${height}; }`\n            : `:root { --docs-banner-height: ${height}; }`}\n        </style>\n      ) : null}\n      {globalKey ? (\n        <style>{`.${globalKey} #${id} { display: none; }`}</style>\n      ) : null}\n      {globalKey ? (\n        // hide a dismissed banner before the page paints\n        <script\n          dangerouslySetInnerHTML={{\n            __html: `if (localStorage.getItem('${globalKey}') === 'true') document.documentElement.classList.add('${globalKey}');`,\n          }}\n        />\n      ) : null}\n\n      {variant === \"rainbow\"\n        ? flow({\n            colors: rainbowColors,\n          })\n        : null}\n      {props.children}\n      {id ? (\n        <Button\n          type=\"button\"\n          variant=\"ghost\"\n          size=\"icon-sm\"\n          aria-label={t(\"Close Banner\", { note: \"aria-label\" })}\n          onClick={onClose}\n          className=\"absolute inset-e-2 top-1/2 -translate-y-1/2 text-muted-foreground\"\n        >\n          <X />\n        </Button>\n      ) : null}\n    </div>\n  );\n}\n\nconst maskImage =\n  \"linear-gradient(to bottom,white,transparent), radial-gradient(circle at top center, white, transparent)\";\n\nfunction flow({ colors }: { colors: string[] }) {\n  return (\n    <>\n      <div\n        className=\"absolute inset-0 -z-1\"\n        style={{\n          maskImage,\n          maskComposite: \"intersect\",\n          animation: \"fd-moving-banner 20s linear infinite\",\n          backgroundImage: `repeating-linear-gradient(70deg, ${[...colors, colors[0]].map((color, i) => `${color} ${(i * 50) / colors.length}%`).join(\", \")})`,\n          backgroundSize: \"200% 100%\",\n          filter: \"saturate(2)\",\n        }}\n      />\n      <style>\n        {`@keyframes fd-moving-banner {\n            from { background-position: 0% 0;  }\n            to { background-position: 100% 0;  }\n         }`}\n      </style>\n    </>\n  );\n}\n\nfunction encodeBase32(str: string) {\n  const alphabet = \"abcdefghijklmnopqrstuvwxyz234567\";\n  let encoded = \"\";\n\n  let buffer = 0;\n  let bitsLeft = 0;\n\n  for (let i = 0; i < str.length; i++) {\n    buffer = (buffer << 8) | str.charCodeAt(i);\n    bitsLeft += 8;\n\n    while (bitsLeft >= 5) {\n      bitsLeft -= 5;\n      encoded += alphabet[(buffer >> bitsLeft) & 31];\n    }\n  }\n\n  if (bitsLeft > 0) {\n    encoded += alphabet[(buffer << (5 - bitsLeft)) & 31];\n  }\n\n  return encoded;\n}\n",
      "type": "registry:component",
      "target": "@components/docs/components/banner.tsx"
    }
  ],
  "type": "registry:component"
}