{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "status-banner",
  "title": "Status Banner",
  "description": "Information, success, warning, and error banners with optional actions and dismissal.",
  "dependencies": [
    "class-variance-authority",
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "https://uaiblocks.dev/r/uai-theme.json",
    "https://uaiblocks.dev/r/uai-utils.json"
  ],
  "files": [
    {
      "path": "src/registry/uai/components/status-banner.tsx",
      "content": "\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { CircleAlert, CircleCheck, Info, TriangleAlert, X } from \"lucide-react\";\nimport {\n  type ComponentProps,\n  createContext,\n  type Ref,\n  useContext,\n  useEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/uai-utils\";\n\nexport const STATUS_BANNER_VARIANTS = [\"card\", \"tinted\", \"bar\"] as const;\nexport type StatusBannerVariant = (typeof STATUS_BANNER_VARIANTS)[number];\nexport type StatusBannerTone = \"info\" | \"success\" | \"warning\" | \"error\";\nexport type StatusBannerProps = ComponentProps<\"div\"> & {\n  variant?: StatusBannerVariant;\n  tone?: StatusBannerTone;\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n};\ntype BannerContext = {\n  tone: StatusBannerTone;\n  variant: StatusBannerVariant;\n  dismiss: () => void;\n};\nconst Context = createContext<BannerContext | null>(null);\nfunction useBanner(part: string) {\n  const context = useContext(Context);\n  if (!context) throw new Error(`${part} must be used within StatusBanner`);\n  return context;\n}\nconst toneClass: Record<StatusBannerTone, string> = {\n  info: \"[--tone:var(--primary)]\",\n  success: \"[--tone:var(--success)]\",\n  warning: \"[--tone:var(--warning)]\",\n  error: \"[--tone:var(--destructive)]\",\n};\nconst toneIcon = {\n  info: Info,\n  success: CircleCheck,\n  warning: TriangleAlert,\n  error: CircleAlert,\n} as const;\n\nfunction assignRef<T>(ref: Ref<T> | undefined, value: T) {\n  if (typeof ref === \"function\") ref(value);\n  else if (ref) ref.current = value;\n}\n\nconst statusBannerVariants = cva(\n  \"flex min-w-0 animate-in flex-wrap border-solid text-[13px]/[18px] text-foreground duration-240 ease-out-quint fade-in-0 slide-in-from-bottom-1 fill-mode-both data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:duration-150 data-[state=closed]:ease-out data-[state=closed]:fill-mode-forwards motion-reduce:animate-none\",\n  {\n    variants: {\n      variant: {\n        card: \"items-start gap-3 rounded-[14px] border bg-card p-3.5\",\n        tinted:\n          \"items-start gap-3 rounded-xl border-0 bg-[color-mix(in_oklab,var(--tone)_11%,var(--card))] px-3.5 py-3 shadow-[inset_0_0_0_1px_color-mix(in_oklab,var(--tone)_18%,transparent)]\",\n        // The bar sits in flow, so it fades for as long as its row collapses.\n        bar: \"items-center gap-2.5 rounded-none data-[state=closed]:duration-300 border-0 border-b border-[color-mix(in_oklab,var(--tone)_22%,var(--border))] bg-[color-mix(in_oklab,var(--tone)_8%,var(--card))] px-4 py-2\",\n      },\n    },\n  },\n);\n\nexport function StatusBanner({\n  variant = \"card\",\n  tone = \"info\",\n  open,\n  defaultOpen = true,\n  onOpenChange,\n  children,\n  className,\n  ref,\n  ...props\n}: StatusBannerProps) {\n  const [internal, setInternal] = useState(defaultOpen);\n  const visible = open ?? internal;\n  const banner = useRef<HTMLDivElement | null>(null);\n  const collapse = useRef<HTMLDivElement | null>(null);\n  // Stay mounted while the exit plays; reduced motion has no animation, so unmount at once.\n  const [rendered, setRendered] = useState(visible);\n  if (visible && !rendered) setRendered(true);\n  useEffect(() => {\n    if (visible) return;\n    const node = banner.current;\n    const animation = node ? window.getComputedStyle(node).animationName : \"\";\n    if (!animation || animation === \"none\") return setRendered(false);\n    // The in-flow bar also gives its row back instead of letting content jump up.\n    collapse.current?.animate?.([{ gridTemplateRows: \"1fr\" }, { gridTemplateRows: \"0fr\" }], {\n      duration: 300,\n      easing: \"cubic-bezier(0.23, 1, 0.32, 1)\",\n      fill: \"forwards\",\n    });\n  }, [visible]);\n  if (!visible && !rendered) return null;\n  const dismiss = () => {\n    if (open === undefined) setInternal(false);\n    onOpenChange?.(false);\n  };\n  const urgent = tone === \"error\" || tone === \"warning\";\n  // Only a closing bar is wrapped, so an open bar stays a direct child (and can be sticky).\n  const closingBar = !visible && variant === \"bar\";\n  const element = (\n    <div\n      role={urgent ? \"alert\" : \"status\"}\n      data-slot=\"status-banner\"\n      className={cn(toneClass[tone], statusBannerVariants({ variant }), className)}\n      {...props}\n      ref={(node) => {\n        banner.current = node;\n        assignRef(ref, node);\n      }}\n      data-variant={variant}\n      data-tone={tone}\n      data-state={visible ? \"open\" : \"closed\"}\n      inert={!visible}\n      onAnimationEnd={(event) => {\n        props.onAnimationEnd?.(event);\n        if (event.target === event.currentTarget && !visible) setRendered(false);\n      }}\n    >\n      {children}\n    </div>\n  );\n  return (\n    <Context.Provider value={{ tone, variant, dismiss }}>\n      {closingBar ? (\n        <div ref={collapse} className=\"grid\">\n          <div className=\"min-h-0 overflow-hidden\">{element}</div>\n        </div>\n      ) : (\n        element\n      )}\n    </Context.Provider>\n  );\n}\n\nexport function StatusBannerIcon({ children, className, ...props }: ComponentProps<\"span\">) {\n  const context = useBanner(\"StatusBannerIcon\");\n  const Icon = toneIcon[context.tone];\n  const chip = context.variant === \"card\";\n  return (\n    <span\n      aria-hidden=\"true\"\n      data-slot=\"status-banner-icon\"\n      className={cn(\n        \"grid flex-none place-items-center rounded-full text-(--tone)\",\n        chip ? \"-my-[5px] size-7 bg-(--tone)/14\" : \"my-0 size-4.5\",\n        className,\n      )}\n      {...props}\n    >\n      {children ?? <Icon size={chip ? 15 : 16} strokeWidth={1.85} />}\n    </span>\n  );\n}\n\nexport function StatusBannerContent({ className, ...props }: ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"status-banner-content\"\n      className={cn(\"grid min-w-0 flex-[1_1_220px] gap-0.5\", className)}\n      {...props}\n    />\n  );\n}\n\nexport function StatusBannerTitle({ className, ...props }: ComponentProps<\"p\">) {\n  return (\n    <p\n      data-slot=\"status-banner-title\"\n      className={cn(\"m-0 font-medium wrap-anywhere\", className)}\n      {...props}\n    />\n  );\n}\n\nexport function StatusBannerDescription({ className, ...props }: ComponentProps<\"p\">) {\n  return (\n    <p\n      data-slot=\"status-banner-description\"\n      className={cn(\"m-0 text-[12.5px] text-pretty text-muted-foreground wrap-anywhere\", className)}\n      {...props}\n    />\n  );\n}\n\nexport function StatusBannerActions({ className, ...props }: ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"status-banner-actions\"\n      className={cn(\"ml-auto flex flex-wrap items-center gap-1.5\", className)}\n      {...props}\n    />\n  );\n}\n\nconst actionVariants = cva(\n  \"cursor-pointer rounded-full py-0 text-foreground transition-[background-color,scale] duration-[120ms,140ms] ease-[ease-out,cubic-bezier(0.23,1,0.32,1)] focus-visible:ring-0 focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-offset-2 focus-visible:outline-ring active:scale-[0.97] motion-reduce:transition-none motion-reduce:active:scale-100\",\n  {\n    variants: {\n      variant: {\n        card: \"h-7 bg-secondary px-3 text-[12.5px]/4 hover:bg-[color-mix(in_oklab,var(--secondary)_85%,var(--foreground))] has-[>svg]:px-3\",\n        tinted: \"h-7 bg-foreground/8 px-3 text-[12.5px]/4 hover:bg-foreground/14 has-[>svg]:px-3\",\n        bar: \"h-6 bg-secondary px-2.5 text-xs/4 hover:bg-[color-mix(in_oklab,var(--secondary)_85%,var(--foreground))] has-[>svg]:px-2.5\",\n      },\n    },\n  },\n);\n\nexport function StatusBannerAction({ className, ...props }: ComponentProps<\"button\">) {\n  const context = useBanner(\"StatusBannerAction\");\n  return (\n    <Button\n      type=\"button\"\n      variant=\"secondary\"\n      data-slot=\"status-banner-action\"\n      className={cn(actionVariants({ variant: context.variant }), className)}\n      {...props}\n    />\n  );\n}\n\nexport function StatusBannerDismiss({\n  children = <X size={14} className=\"size-3.5\" aria-hidden=\"true\" />,\n  onClick,\n  className,\n  ...props\n}: ComponentProps<\"button\">) {\n  const context = useBanner(\"StatusBannerDismiss\");\n  return (\n    <Button\n      variant=\"ghost\"\n      size=\"icon\"\n      aria-label=\"Dismiss\"\n      data-slot=\"status-banner-dismiss\"\n      className={cn(\n        \"flex-none cursor-pointer rounded-lg text-subtle-foreground transition-[background-color,color,scale] duration-[120ms,120ms,140ms] ease-[ease-out,ease-out,cubic-bezier(0.23,1,0.32,1)] hover:bg-foreground/8 hover:text-foreground focus-visible:ring-0 focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-offset-2 focus-visible:outline-ring active:scale-[0.94] motion-reduce:transition-none motion-reduce:active:scale-100 dark:hover:bg-foreground/8 [&_svg:not([class*='size-'])]:size-3.5\",\n        context.variant === \"bar\"\n          ? \"-my-0.75 mr-[-4px] ml-0 size-6\"\n          : \"-my-[5px] mr-[-6px] ml-0 size-7\",\n        className,\n      )}\n      {...props}\n      type=\"button\"\n      onClick={(event) => {\n        onClick?.(event);\n        if (!event.defaultPrevented) context.dismiss();\n      }}\n    >\n      {children}\n    </Button>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@ui/uai/status-banner.tsx"
    }
  ],
  "categories": [
    "feedback",
    "status"
  ],
  "type": "registry:ui"
}