{
  "name": "banner",
  "type": "registry:ui",
  "dependencies": [
    "radix-ui"
  ],
  "registryDependencies": [
    "button",
    "@diceui/use-as-ref",
    "@diceui/use-lazy-ref"
  ],
  "files": [
    {
      "path": "ui/banner.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { X } from \"lucide-react\";\nimport { Slot as SlotPrimitive } from \"radix-ui\";\nimport * as React from \"react\";\nimport * as ReactDOM from \"react-dom\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useAsRef } from \"@/registry/bases/radix/hooks/use-as-ref\";\nimport { useLazyRef } from \"@/registry/bases/radix/hooks/use-lazy-ref\";\nimport { Button } from \"@/registry/bases/radix/ui/button\";\n\nconst BANNER_ANIMATION_DURATION = 400;\nconst DEFAULT_BANNER_PRIORITY = 0;\nconst DEFAULT_BANNER_DISMISSIBLE = true;\n\ntype BannerVariant = \"default\" | \"info\" | \"success\" | \"warning\" | \"destructive\";\ntype BannerSide = \"top\" | \"bottom\";\ntype BannerStrategy = \"fixed\" | \"static\" | \"sticky\" | \"absolute\";\n\ninterface DivProps extends React.ComponentProps<\"div\"> {\n  asChild?: boolean;\n}\n\ntype CloseElement = React.ComponentRef<typeof BannerClose>;\n\ninterface BannerRenderProps {\n  id: string;\n  variant?: BannerVariant;\n  dismissible: boolean;\n  onClose: () => void;\n  onRemove: () => void;\n}\n\ntype BannerContent =\n  | React.ReactNode\n  | ((props: BannerRenderProps) => React.ReactNode);\n\ninterface BannerData {\n  id: string;\n  content: BannerContent;\n  variant?: BannerVariant;\n  priority?: number;\n  duration?: number;\n  dismissible?: boolean;\n  onDismiss?: () => void;\n}\n\ninterface StoreState {\n  banners: BannerData[];\n  removing: Set<string>;\n  heights: Map<string, number>;\n}\n\ninterface Store {\n  subscribe: (callback: () => void) => () => void;\n  getState: () => StoreState;\n  notify: () => void;\n  onBannerAdd: (banner: Omit<BannerData, \"id\">) => string;\n  onBannerRemove: (id: string) => void;\n  onBannersClear: () => void;\n  onRemovingChange: (id: string, value: boolean) => void;\n  onHeightChange: (id: string, height: number) => void;\n  onHeightRemove: (id: string) => void;\n}\n\nconst StoreContext = React.createContext<Store | null>(null);\n\nfunction useStoreContext(consumerName: string) {\n  const context = React.useContext(StoreContext);\n  if (!context) {\n    throw new Error(`\\`${consumerName}\\` must be used within \\`Banners\\``);\n  }\n  return context;\n}\n\nfunction useStore<T>(store: Store, selector: (state: StoreState) => T): T {\n  return React.useSyncExternalStore(\n    store.subscribe,\n    () => selector(store.getState()),\n    () => selector(store.getState()),\n  );\n}\n\ninterface BannerContextValue {\n  id?: string;\n  variant?: BannerVariant | null;\n  dismissible?: boolean;\n  onClose?: () => void;\n}\n\nconst BannerContext = React.createContext<BannerContextValue | null>(null);\n\nfunction useBannerContext(consumerName: string) {\n  const context = React.useContext(BannerContext);\n  if (!context) {\n    throw new Error(`\\`${consumerName}\\` must be used within \\`Banner\\``);\n  }\n  return context;\n}\n\nfunction useBanner() {\n  const { id, variant, dismissible, onClose } = useBannerContext(\"useBanner\");\n  const storeContext = React.useContext(StoreContext);\n\n  return React.useMemo(() => {\n    const onRemove =\n      id && storeContext ? () => storeContext.onBannerRemove(id) : undefined;\n\n    return {\n      id,\n      variant,\n      dismissible,\n      onClose,\n      onRemove,\n    };\n  }, [id, variant, dismissible, onClose, storeContext]);\n}\n\ninterface BannersProps {\n  children?: React.ReactNode;\n  maxVisible?: number;\n  side?: BannerSide;\n  strategy?: BannerStrategy;\n  container?: Element | DocumentFragment | null;\n}\n\nfunction Banners(props: BannersProps) {\n  const {\n    children,\n    maxVisible = 1,\n    side = \"top\",\n    strategy = \"fixed\",\n    container: containerProp,\n  } = props;\n\n  const stateRef = useLazyRef<StoreState>(() => ({\n    banners: [],\n    removing: new Set(),\n    heights: new Map(),\n  }));\n  const listenersRef = useLazyRef<Set<() => void>>(() => new Set());\n  const timeoutsRef = useLazyRef<Map<string, ReturnType<typeof setTimeout>>>(\n    () => new Map(),\n  );\n\n  const store: Store = React.useMemo(\n    () => ({\n      subscribe: (cb) => {\n        listenersRef.current.add(cb);\n        return () => listenersRef.current.delete(cb);\n      },\n      getState: () => stateRef.current,\n      notify: () => {\n        for (const listener of listenersRef.current) {\n          listener();\n        }\n      },\n      onBannerAdd: (banner) => {\n        const id = crypto.randomUUID();\n        const newBanner: BannerData = { ...banner, id };\n        const priority = banner.priority ?? DEFAULT_BANNER_PRIORITY;\n\n        const banners = [...stateRef.current.banners];\n        const insertIndex = banners.findIndex(\n          (b) => (b.priority ?? DEFAULT_BANNER_PRIORITY) < priority,\n        );\n\n        if (insertIndex === -1) {\n          banners.push(newBanner);\n        } else {\n          banners.splice(insertIndex, 0, newBanner);\n        }\n\n        stateRef.current.banners = banners;\n        store.notify();\n\n        if (banner.duration && banner.duration > 0) {\n          const timeoutId = setTimeout(() => {\n            store.onRemovingChange(id, true);\n            timeoutsRef.current.delete(id);\n          }, banner.duration);\n          timeoutsRef.current.set(id, timeoutId);\n        }\n\n        return id;\n      },\n      onBannerRemove: (id) => {\n        const banner = stateRef.current.banners.find((b) => b.id === id);\n        if (!banner) return;\n\n        const timeoutId = timeoutsRef.current.get(id);\n        if (timeoutId) {\n          clearTimeout(timeoutId);\n          timeoutsRef.current.delete(id);\n        }\n\n        const newRemoving = new Set(stateRef.current.removing);\n        newRemoving.delete(id);\n        stateRef.current.removing = newRemoving;\n\n        banner.onDismiss?.();\n        stateRef.current.banners = stateRef.current.banners.filter(\n          (b) => b.id !== id,\n        );\n        store.notify();\n      },\n      onBannersClear: () => {\n        for (const timeoutId of timeoutsRef.current.values()) {\n          clearTimeout(timeoutId);\n        }\n        timeoutsRef.current.clear();\n        stateRef.current.removing = new Set();\n        stateRef.current.heights = new Map();\n        stateRef.current.banners = [];\n        store.notify();\n      },\n      onRemovingChange: (id, value) => {\n        const newSet = new Set(stateRef.current.removing);\n        if (value) {\n          newSet.add(id);\n        } else {\n          newSet.delete(id);\n        }\n        stateRef.current.removing = newSet;\n        store.notify();\n      },\n      onHeightChange: (id, height) => {\n        if (stateRef.current.heights.get(id) === height) return;\n        const newHeights = new Map(stateRef.current.heights);\n        newHeights.set(id, height);\n        stateRef.current.heights = newHeights;\n        store.notify();\n      },\n      onHeightRemove: (id) => {\n        if (!stateRef.current.heights.has(id)) return;\n        const newHeights = new Map(stateRef.current.heights);\n        newHeights.delete(id);\n        stateRef.current.heights = newHeights;\n        store.notify();\n      },\n    }),\n    [stateRef, listenersRef, timeoutsRef],\n  );\n\n  const banners = useStore(store, (state) => state.banners);\n  const heights = useStore(store, (state) => state.heights);\n  const visibleBanners = banners.slice(0, maxVisible);\n\n  const withPortal = strategy === \"fixed\" || strategy === \"absolute\";\n  const container = withPortal\n    ? (containerProp ?? globalThis.document?.body ?? null)\n    : null;\n\n  const totalHeight = React.useMemo(() => {\n    let total = 0;\n    for (const banner of visibleBanners) {\n      total += heights.get(banner.id) ?? 0;\n    }\n    return total;\n  }, [visibleBanners, heights]);\n\n  const bannerContainer = visibleBanners.length > 0 && (\n    <div\n      data-slot=\"banner-container\"\n      data-side={side}\n      data-strategy={strategy}\n      className={cn(\n        \"pointer-events-none right-0 left-0 isolate z-50\",\n        strategy === \"fixed\" && \"fixed\",\n        strategy === \"static\" && \"relative\",\n        strategy === \"sticky\" && \"sticky\",\n        strategy === \"absolute\" && \"absolute\",\n        side === \"top\" ? \"top-0\" : \"bottom-0\",\n      )}\n      style={{\n        height: totalHeight > 0 ? totalHeight : \"auto\",\n        transition: `height ${BANNER_ANIMATION_DURATION}ms cubic-bezier(0.32, 0.72, 0, 1)`,\n      }}\n    >\n      {visibleBanners.map((banner, index) => (\n        <BannerImpl key={banner.id} banner={banner} side={side} index={index} />\n      ))}\n    </div>\n  );\n\n  return (\n    <StoreContext.Provider value={store}>\n      {strategy === \"static\" || strategy === \"sticky\" ? (\n        <>\n          {side === \"top\" && bannerContainer}\n          {children}\n          {side === \"bottom\" && bannerContainer}\n        </>\n      ) : (\n        <>\n          {children}\n          {container &&\n            bannerContainer &&\n            ReactDOM.createPortal(bannerContainer, container)}\n        </>\n      )}\n    </StoreContext.Provider>\n  );\n}\n\nfunction useBanners() {\n  const store = useStoreContext(\"useBanners\");\n  const banners = useStore(store, (state) => state.banners);\n\n  return React.useMemo(\n    () => ({\n      onBannerAdd: store.onBannerAdd,\n      onBannerRemove: store.onBannerRemove,\n      onBannersClear: store.onBannersClear,\n      banners,\n    }),\n    [store, banners],\n  );\n}\n\nconst bannerVariants = cva(\n  \"pointer-events-auto relative flex w-full items-center gap-3 border-b px-4 py-3 text-sm motion-reduce:transition-none\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-card text-card-foreground\",\n        info: \"bg-blue-50 text-blue-900 dark:bg-blue-950 dark:text-blue-50\",\n        success:\n          \"bg-green-50 text-green-900 dark:bg-green-950 dark:text-green-50\",\n        warning:\n          \"bg-yellow-50 text-yellow-900 dark:bg-yellow-950 dark:text-yellow-50\",\n        destructive: \"bg-red-50 text-red-900 dark:bg-red-950 dark:text-red-50\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n    },\n  },\n);\n\ninterface BannerImplProps {\n  banner: BannerData;\n  side: BannerSide;\n  index: number;\n}\n\nfunction BannerImpl(props: BannerImplProps) {\n  const { banner, side, index } = props;\n\n  const store = useStoreContext(\"BannerImpl\");\n  const removing = useStore(store, (state) => state.removing.has(banner.id));\n  const banners = useStore(store, (state) => state.banners);\n  const heights = useStore(store, (state) => state.heights);\n\n  const [mounted, setMounted] = React.useState(false);\n  const bannerRef = React.useRef<HTMLDivElement>(null);\n  const offsetBeforeRemoveRef = React.useRef(0);\n\n  const offset = React.useMemo(() => {\n    let total = 0;\n    for (const b of banners) {\n      if (b.id === banner.id) break;\n      total += heights.get(b.id) ?? 0;\n    }\n    return total;\n  }, [banners, heights, banner.id]);\n\n  if (!removing) {\n    offsetBeforeRemoveRef.current = offset;\n  }\n\n  React.useEffect(() => {\n    const frame = requestAnimationFrame(() => setMounted(true));\n    return () => cancelAnimationFrame(frame);\n  }, []);\n\n  React.useLayoutEffect(() => {\n    if (!bannerRef.current || removing) return;\n    const height = bannerRef.current.getBoundingClientRect().height;\n    store.onHeightChange(banner.id, height);\n  }, [store, banner.id, removing]);\n\n  React.useEffect(() => {\n    if (!removing) return;\n    store.onHeightRemove(banner.id);\n    const timeoutId = setTimeout(\n      () => store.onBannerRemove(banner.id),\n      BANNER_ANIMATION_DURATION,\n    );\n    return () => clearTimeout(timeoutId);\n  }, [removing, store, banner.id]);\n\n  const onClose = React.useCallback(\n    () => store.onRemovingChange(banner.id, true),\n    [store, banner.id],\n  );\n\n  const onRemove = React.useCallback(\n    () => store.onBannerRemove(banner.id),\n    [store, banner.id],\n  );\n\n  const dismissible = banner.dismissible ?? DEFAULT_BANNER_DISMISSIBLE;\n\n  const contextValue = React.useMemo<BannerContextValue>(\n    () => ({ id: banner.id, variant: banner.variant, dismissible, onClose }),\n    [banner.id, banner.variant, dismissible, onClose],\n  );\n\n  const renderProps = React.useMemo<BannerRenderProps>(\n    () => ({\n      id: banner.id,\n      variant: banner.variant,\n      dismissible,\n      onClose,\n      onRemove,\n    }),\n    [banner.id, banner.variant, dismissible, onClose, onRemove],\n  );\n\n  const currentOffset = removing ? offsetBeforeRemoveRef.current : offset;\n  const isTop = side === \"top\";\n\n  function getTransform() {\n    if (!mounted) return isTop ? \"translateY(-100%)\" : \"translateY(100%)\";\n    if (removing) {\n      return isTop\n        ? `translateY(calc(${currentOffset}px - 100%))`\n        : `translateY(calc(-${currentOffset}px + 100%))`;\n    }\n    return isTop\n      ? `translateY(${currentOffset}px)`\n      : `translateY(-${currentOffset}px)`;\n  }\n\n  return (\n    <BannerContext.Provider value={contextValue}>\n      <div\n        role=\"status\"\n        aria-live=\"polite\"\n        data-slot=\"queued-banner\"\n        data-state={removing ? \"closed\" : \"open\"}\n        data-mounted={mounted}\n        data-removed={removing}\n        data-side={side}\n        data-front={index === 0}\n        data-index={index}\n        ref={bannerRef}\n        className={bannerVariants({ variant: banner.variant })}\n        style={{\n          position: \"absolute\",\n          [isTop ? \"top\" : \"bottom\"]: 0,\n          left: 0,\n          right: 0,\n          zIndex: removing ? 0 : 50 - index,\n          transform: getTransform(),\n          opacity: mounted && !removing ? 1 : 0,\n          transition: `transform ${BANNER_ANIMATION_DURATION}ms cubic-bezier(0.32, 0.72, 0, 1), opacity ${removing ? BANNER_ANIMATION_DURATION / 2 : BANNER_ANIMATION_DURATION}ms ease`,\n        }}\n      >\n        {typeof banner.content === \"function\"\n          ? banner.content(renderProps)\n          : banner.content}\n      </div>\n    </BannerContext.Provider>\n  );\n}\n\ninterface BannerProps extends DivProps, VariantProps<typeof bannerVariants> {\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  onDismiss?: () => void;\n  priority?: number;\n  duration?: number;\n  dismissible?: boolean;\n}\n\nfunction Banner(props: BannerProps) {\n  const {\n    className,\n    variant = \"default\",\n    open: openProp,\n    defaultOpen = true,\n    onOpenChange,\n    onDismiss,\n    priority,\n    duration,\n    dismissible = DEFAULT_BANNER_DISMISSIBLE,\n    children,\n    asChild,\n    ...rootProps\n  } = props;\n\n  const store = React.useContext(StoreContext);\n\n  const isInsideStore = store !== null;\n  const isControlled = openProp !== undefined;\n\n  const openRef = useLazyRef(() => openProp ?? defaultOpen);\n  const listenersRef = useLazyRef<Set<() => void>>(() => new Set());\n  const bannerIdRef = React.useRef<string | null>(null);\n  const onDismissRef = useAsRef(onDismiss);\n  const onOpenChangeRef = useAsRef(onOpenChange);\n\n  if (isControlled) {\n    openRef.current = openProp;\n  }\n\n  const subscribe = React.useCallback(\n    (cb: () => void) => {\n      listenersRef.current.add(cb);\n      return () => listenersRef.current.delete(cb);\n    },\n    [listenersRef],\n  );\n\n  const getSnapshot = React.useCallback(() => openRef.current, [openRef]);\n\n  const open = React.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n\n  React.useEffect(() => {\n    if (!isInsideStore || !store || !open) return;\n\n    const id = store.onBannerAdd({\n      content: children,\n      variant: variant ?? undefined,\n      priority,\n      dismissible,\n      duration,\n      onDismiss: () => {\n        onDismissRef.current?.();\n        onOpenChangeRef.current?.(false);\n      },\n    });\n    bannerIdRef.current = id;\n\n    return () => {\n      if (bannerIdRef.current) {\n        store.onBannerRemove(bannerIdRef.current);\n        bannerIdRef.current = null;\n      }\n    };\n  }, [\n    isInsideStore,\n    store,\n    open,\n    children,\n    variant,\n    priority,\n    dismissible,\n    duration,\n    onDismissRef,\n    onOpenChangeRef,\n  ]);\n\n  const onClose = React.useCallback(() => {\n    if (!isControlled) {\n      openRef.current = false;\n      for (const listener of listenersRef.current) {\n        listener();\n      }\n    }\n    onOpenChangeRef.current?.(false);\n  }, [isControlled, openRef, listenersRef, onOpenChangeRef]);\n\n  const contextValue = React.useMemo<BannerContextValue>(\n    () => ({\n      variant,\n      dismissible,\n      onClose,\n    }),\n    [variant, dismissible, onClose],\n  );\n\n  if (!open || isInsideStore) return null;\n\n  const RootPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <BannerContext.Provider value={contextValue}>\n      <RootPrimitive\n        role=\"status\"\n        aria-live=\"polite\"\n        data-slot=\"banner\"\n        data-state=\"open\"\n        className={cn(bannerVariants({ variant, className }))}\n        {...rootProps}\n      >\n        {children}\n      </RootPrimitive>\n    </BannerContext.Provider>\n  );\n}\n\nfunction BannerIcon(props: DivProps) {\n  const { className, asChild, ...iconProps } = props;\n\n  const IconPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <IconPrimitive\n      data-slot=\"banner-icon\"\n      className={cn(\"flex shrink-0 items-center [&>svg]:size-4\", className)}\n      {...iconProps}\n    />\n  );\n}\n\nfunction BannerContent(props: DivProps) {\n  const { className, asChild, ...contentProps } = props;\n\n  const ContentPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <ContentPrimitive\n      data-slot=\"banner-content\"\n      className={cn(\"flex min-w-0 flex-1 flex-col gap-1\", className)}\n      {...contentProps}\n    />\n  );\n}\n\nfunction BannerTitle(props: React.ComponentProps<\"div\">) {\n  const { className, ...titleProps } = props;\n\n  return (\n    <div\n      data-slot=\"banner-title\"\n      className={cn(\"text-sm leading-none font-medium\", className)}\n      {...titleProps}\n    />\n  );\n}\n\nfunction BannerDescription(props: React.ComponentProps<\"div\">) {\n  const { className, ...descriptionProps } = props;\n\n  return (\n    <div\n      data-slot=\"banner-description\"\n      className={cn(\"text-xs opacity-90\", className)}\n      {...descriptionProps}\n    />\n  );\n}\n\nfunction BannerActions(props: DivProps) {\n  const { className, asChild, ...actionsProps } = props;\n\n  const ActionsPrimitive = asChild ? SlotPrimitive.Slot : \"div\";\n\n  return (\n    <ActionsPrimitive\n      data-slot=\"banner-actions\"\n      className={cn(\"flex items-center gap-2\", className)}\n      {...actionsProps}\n    />\n  );\n}\n\nfunction BannerClose(props: React.ComponentProps<typeof Button>) {\n  const { onClick: onClickProp, disabled, children, ...closeProps } = props;\n\n  const { dismissible = DEFAULT_BANNER_DISMISSIBLE, onClose } =\n    useBannerContext(\"BannerClose\");\n\n  const isDisabled = disabled ?? !dismissible;\n\n  const onClick = React.useCallback(\n    (event: React.MouseEvent<CloseElement>) => {\n      onClickProp?.(event);\n      if (event.defaultPrevented || isDisabled) return;\n      onClose?.();\n    },\n    [onClickProp, isDisabled, onClose],\n  );\n\n  return (\n    <Button\n      data-slot=\"banner-close\"\n      variant=\"ghost\"\n      size=\"icon-sm\"\n      onClick={onClick}\n      disabled={isDisabled}\n      {...closeProps}\n    >\n      {children ?? <X className=\"size-3.5\" />}\n    </Button>\n  );\n}\n\nexport {\n  Banner,\n  BannerActions,\n  BannerClose,\n  BannerContent,\n  BannerDescription,\n  BannerIcon,\n  Banners,\n  BannerTitle,\n  useBanner,\n  useBanners,\n};\n",
      "target": ""
    }
  ]
}