{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "banner",
  "title": "Banner",
  "description": "Full-width announcement bar with variants, dismissal, and an optional action slot.",
  "dependencies": [
    "@vllnt/ui@^0.3.0",
    "lucide-react",
    "class-variance-authority",
    "@radix-ui/react-slot"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/banner/banner.tsx",
      "content": "\"use client\";\n\nimport {\n  type ButtonHTMLAttributes,\n  type ComponentPropsWithoutRef,\n  type ReactNode,\n  useCallback,\n  useState,\n  useSyncExternalStore,\n} from \"react\";\n\nimport { Slot } from \"@radix-ui/react-slot\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { X } from \"lucide-react\";\n\nimport { cn } from \"@vllnt/ui\";\n\nconst bannerVariants = cva(\n  \"flex w-full items-start gap-3 border-b px-4 py-3 text-sm transition-all motion-safe:animate-in motion-safe:slide-in-from-top-1\",\n  {\n    defaultVariants: {\n      variant: \"info\",\n    },\n    variants: {\n      variant: {\n        destructive:\n          \"border-destructive/50 bg-destructive/10 text-destructive dark:border-destructive [&_svg]:text-destructive\",\n        info: \"border-border bg-muted text-foreground [&_svg]:text-muted-foreground\",\n        success:\n          \"border-emerald-500/40 bg-emerald-500/10 text-emerald-900 dark:text-emerald-200 [&_svg]:text-emerald-600 dark:[&_svg]:text-emerald-300\",\n        warning:\n          \"border-amber-500/40 bg-amber-500/10 text-amber-900 dark:text-amber-200 [&_svg]:text-amber-600 dark:[&_svg]:text-amber-300\",\n      },\n    },\n  },\n);\n\n/**\n * Visual variant for {@link Banner}. Drives both color treatment and the ARIA\n * role: `warning` and `destructive` render as `role=\"alert\"`, others as\n * `role=\"status\"`.\n *\n * @public\n */\nexport type BannerVariant = \"destructive\" | \"info\" | \"success\" | \"warning\";\n\nconst URGENT_VARIANTS: ReadonlySet<BannerVariant> = new Set([\n  \"destructive\",\n  \"warning\",\n]);\n\nconst STORAGE_PREFIX = \"vllnt-ui:banner-dismissed:\";\n\nfunction safeStorageGet(key: string): null | string {\n  if (typeof window === \"undefined\") return null;\n  try {\n    return window.localStorage.getItem(key);\n  } catch {\n    return null;\n  }\n}\n\nfunction safeStorageSet(key: string, value: string): void {\n  if (typeof window === \"undefined\") return;\n  try {\n    window.localStorage.setItem(key, value);\n  } catch {\n    return;\n  }\n}\n\nfunction subscribeToStorage(callback: () => void): () => void {\n  if (typeof window === \"undefined\") {\n    return () => {\n      return;\n    };\n  }\n  window.addEventListener(\"storage\", callback);\n  return () => {\n    window.removeEventListener(\"storage\", callback);\n  };\n}\n\nfunction usePersistedDismissed(storageKey: string | undefined): boolean {\n  const getClientSnapshot = useCallback(() => {\n    if (!storageKey) return false;\n    return safeStorageGet(storageKey) === \"1\";\n  }, [storageKey]);\n  const getServerSnapshot = useCallback(() => false, []);\n  return useSyncExternalStore(\n    subscribeToStorage,\n    getClientSnapshot,\n    getServerSnapshot,\n  );\n}\n\n/**\n * Props for {@link Banner}.\n *\n * @public\n */\nexport type BannerProps = {\n  /** When true, renders a dismiss control. */\n  dismissible?: boolean;\n  /** Accessible label for the dismiss control. Defaults to `\"Dismiss\"`. */\n  dismissLabel?: string;\n  /** Optional icon rendered to the left of the message. */\n  icon?: ReactNode;\n  /**\n   * Stable identifier used as the localStorage key when persisting dismissal.\n   * Pair with `persistDismissal` to remember a user's choice.\n   */\n  id?: string;\n  /** Fires after the user clicks the dismiss control. */\n  onDismiss?: () => void;\n  /**\n   * When true alongside `id`, persists dismissal in localStorage so the\n   * banner remains hidden across reloads.\n   */\n  persistDismissal?: boolean;\n} & ComponentPropsWithoutRef<\"div\"> &\n  VariantProps<typeof bannerVariants>;\n\n/**\n * Full-width announcement bar.\n *\n * Renders a horizontal bar with variant-driven color treatment, an optional\n * icon slot, and an optional dismiss control. Pair `id` with\n * `persistDismissal` to remember dismissal across sessions in localStorage.\n *\n * @example\n * ```tsx\n * <Banner variant=\"warning\" dismissible icon={<AlertTriangle />}>\n *   Scheduled maintenance tonight at 11pm UTC.\n *   <BannerAction onClick={openStatus}>View status</BannerAction>\n * </Banner>\n * ```\n *\n * @public\n */\nexport const Banner = ({\n  children,\n  className,\n  dismissible = false,\n  dismissLabel = \"Dismiss\",\n  icon,\n  id,\n  onDismiss,\n  persistDismissal = false,\n  ref,\n  role: roleOverride,\n  variant,\n  ...rest\n}: BannerProps & { ref?: React.Ref<HTMLDivElement> }) => {\n  const storageKey =\n    persistDismissal && id ? `${STORAGE_PREFIX}${id}` : undefined;\n  const persistedDismissed = usePersistedDismissed(storageKey);\n  const [locallyDismissed, setLocallyDismissed] = useState(false);\n\n  const handleDismiss = useCallback(() => {\n    setLocallyDismissed(true);\n    if (storageKey) safeStorageSet(storageKey, \"1\");\n    onDismiss?.();\n  }, [onDismiss, storageKey]);\n\n  if (locallyDismissed || persistedDismissed) return null;\n\n  const resolvedVariant: BannerVariant = variant ?? \"info\";\n  const role =\n    roleOverride ?? (URGENT_VARIANTS.has(resolvedVariant) ? \"alert\" : \"status\");\n\n  return (\n    <div\n      className={cn(bannerVariants({ variant }), className)}\n      id={id}\n      ref={ref}\n      role={role}\n      {...rest}\n    >\n      {icon ? (\n        <span\n          aria-hidden=\"true\"\n          className=\"mt-0.5 flex size-4 shrink-0 items-center justify-center [&>svg]:h-4 [&>svg]:w-4\"\n        >\n          {icon}\n        </span>\n      ) : null}\n      <div className=\"flex min-w-0 flex-1 flex-wrap items-center gap-x-3 gap-y-1\">\n        {children}\n      </div>\n      {dismissible ? (\n        <button\n          aria-label={dismissLabel}\n          className=\"ml-auto inline-flex size-6 shrink-0 items-center justify-center rounded-md opacity-70 transition-colors hover:bg-foreground/10 hover:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\"\n          onClick={handleDismiss}\n          type=\"button\"\n        >\n          <X aria-hidden=\"true\" className=\"size-4\" />\n        </button>\n      ) : null}\n    </div>\n  );\n};\nBanner.displayName = \"Banner\";\n\n/**\n * Props for {@link BannerAction}.\n *\n * @public\n */\nexport type BannerActionProps = {\n  /** Render the wrapped child element instead of a `<button>`. */\n  asChild?: boolean;\n} & ButtonHTMLAttributes<HTMLButtonElement>;\n\n/**\n * Action slot used inside a {@link Banner} body. Renders a styled button by\n * default, or composes onto a passed child element when `asChild` is true\n * (e.g. wrap an `<a>` for link actions).\n *\n * @public\n */\nexport const BannerAction = ({\n  asChild = false,\n  children,\n  className,\n  ref,\n  type,\n  ...rest\n}: BannerActionProps & { ref?: React.Ref<HTMLButtonElement> }) => {\n  const Comp = asChild ? Slot : \"button\";\n  const buttonProps: ButtonHTMLAttributes<HTMLButtonElement> = asChild\n    ? rest\n    : { type: type ?? \"button\", ...rest };\n\n  return (\n    <Comp\n      className={cn(\n        \"inline-flex h-7 items-center justify-center rounded-md border border-foreground/20 bg-transparent px-3 text-xs font-medium underline-offset-4 transition-colors hover:bg-foreground/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2\",\n        className,\n      )}\n      ref={ref}\n      {...buttonProps}\n    >\n      {children}\n    </Comp>\n  );\n};\nBannerAction.displayName = \"BannerAction\";\n\nexport { bannerVariants };\n",
      "type": "registry:component"
    }
  ],
  "type": "registry:component",
  "version": "0.3.0",
  "stability": "stable"
}
