{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "banner",
  "type": "registry:component",
  "title": "Banner",
  "description": "Full-width announcement bar with variants, dismissal, and an optional action slot.",
  "dependencies": [
    "@vllnt/ui@^0.2.1",
    "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  forwardRef,\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 = forwardRef<HTMLDivElement, BannerProps>(\n  (\n    {\n      children,\n      className,\n      dismissible = false,\n      dismissLabel = \"Dismiss\",\n      icon,\n      id,\n      onDismiss,\n      persistDismissal = false,\n      role: roleOverride,\n      variant,\n      ...rest\n    },\n    ref,\n  ) => {\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 ??\n      (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  },\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 = forwardRef<HTMLButtonElement, BannerActionProps>(\n  ({ asChild = false, children, className, type, ...rest }, ref) => {\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  },\n);\nBannerAction.displayName = \"BannerAction\";\n\nexport { bannerVariants };\n",
      "type": "registry:component"
    }
  ],
  "version": "0.2.1",
  "stability": "stable"
}
