{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "banner",
  "type": "registry:ui",
  "registryDependencies": [],
  "dependencies": ["motion", "lucide-react"],
  "devDependencies": [],
  "files": [
    {
      "path": "banner.tsx",
      "content": "\"use client\";\n\nimport {\n  ArrowRight,\n  Check,\n  CircleAlert,\n  CircleCheck,\n  Info,\n  OctagonAlert,\n  X,\n} from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport {\n  type ReactNode,\n  useCallback,\n  useEffect,\n  useRef,\n  useState,\n} from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst DEFAULT_MORPH_DURATION_MS = 2400;\n\n/** Bouncy-but-controlled spring driving the bar-to-pill layout morph. */\nconst MORPH_SPRING = {\n  type: \"spring\",\n  stiffness: 320,\n  damping: 30,\n  mass: 0.8,\n} as const;\n\n/** Ease used for the entrance and dismiss height collapse. */\nconst COLLAPSE_TRANSITION = {\n  duration: 0.45,\n  ease: [0.32, 0.72, 0, 1],\n} as const;\n\n/**\n * Slow, constant-speed light sweep. The keyframes barely overshoot the\n * surface, so the band glides across at one steady pace and loops forever\n * without a fast off-screen catch-up.\n */\nconst SHEEN_KEYFRAMES = [\"120% 0%\", \"-20% 0%\"];\n\nconst SHEEN_TRANSITION = {\n  duration: 6,\n  ease: \"linear\",\n  repeat: Number.POSITIVE_INFINITY,\n} as const;\n\nexport type BannerVariant = \"default\" | \"info\" | \"success\" | \"error\";\n\ntype VariantStyle = {\n  surface: string;\n  ring: string;\n  action: string;\n  dismiss: string;\n};\n\nconst VARIANT_STYLES: Record<BannerVariant, VariantStyle> = {\n  default: {\n    surface:\n      \"border-zinc-800/80 bg-gradient-to-r from-zinc-950 via-zinc-900 to-zinc-950 text-zinc-50 dark:border-zinc-300/70 dark:from-zinc-100 dark:via-white dark:to-zinc-100 dark:text-zinc-950\",\n    ring: \"border-white/25 text-zinc-100 dark:border-zinc-900/25 dark:text-zinc-900\",\n    action:\n      \"border-white/40 text-white hover:bg-white/10 dark:border-zinc-900/30 dark:text-zinc-900 dark:hover:bg-zinc-900/[0.06]\",\n    dismiss:\n      \"text-zinc-400 hover:bg-white/10 hover:text-white dark:text-zinc-500 dark:hover:bg-zinc-900/10 dark:hover:text-zinc-900\",\n  },\n  info: {\n    surface:\n      \"border-sky-800/60 bg-gradient-to-r from-sky-700 via-sky-600 to-indigo-600 text-white\",\n    ring: \"border-white/30 text-white\",\n    action: \"border-white/45 text-white hover:bg-white/15\",\n    dismiss: \"text-white/70 hover:bg-white/15 hover:text-white\",\n  },\n  success: {\n    surface:\n      \"border-emerald-800/60 bg-gradient-to-r from-emerald-700 via-emerald-600 to-teal-600 text-white\",\n    ring: \"border-white/30 text-white\",\n    action: \"border-white/45 text-white hover:bg-white/15\",\n    dismiss: \"text-white/70 hover:bg-white/15 hover:text-white\",\n  },\n  error: {\n    surface:\n      \"border-rose-800/60 bg-gradient-to-r from-rose-700 via-rose-600 to-red-600 text-white\",\n    ring: \"border-white/30 text-white\",\n    action: \"border-white/45 text-white hover:bg-white/15\",\n    dismiss: \"text-white/70 hover:bg-white/15 hover:text-white\",\n  },\n};\n\nconst VARIANT_ICONS: Record<BannerVariant, ReactNode> = {\n  default: <CircleAlert className=\"size-4\" />,\n  info: <Info className=\"size-4\" />,\n  success: <CircleCheck className=\"size-4\" />,\n  error: <OctagonAlert className=\"size-4\" />,\n};\n\n/** Crossfade-with-blur used when the bar content swaps to the pill content. */\nconst contentVariants = {\n  initial: { opacity: 0, scale: 0.9, filter: \"blur(4px)\" },\n  animate: { opacity: 1, scale: 1, filter: \"blur(0px)\" },\n  exit: { opacity: 0, scale: 0.9, filter: \"blur(4px)\" },\n};\n\n/** Soft light sweep that loops across the gradient surface. */\nfunction BannerSheen() {\n  return (\n    <motion.span\n      animate={{ backgroundPosition: SHEEN_KEYFRAMES }}\n      aria-hidden\n      className=\"pointer-events-none absolute inset-0 bg-[length:250%_100%] bg-[linear-gradient(110deg,transparent_30%,rgba(255,255,255,0.1)_50%,transparent_70%)] dark:bg-[linear-gradient(110deg,transparent_30%,rgba(255,255,255,0.22)_50%,transparent_70%)]\"\n      transition={SHEEN_TRANSITION}\n    />\n  );\n}\n\nfunction BannerAction({\n  label,\n  href,\n  onClick,\n  actionClass,\n  reducedMotion,\n}: {\n  label: string;\n  href?: string;\n  onClick: () => void;\n  actionClass: string;\n  reducedMotion: boolean;\n}) {\n  const sharedClass = cn(\n    \"shrink-0 rounded-lg border bg-transparent px-3.5 py-1.5 font-medium text-xs outline-none transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-ring/60\",\n    actionClass\n  );\n  const whileTap = reducedMotion ? undefined : { scale: 0.96 };\n\n  if (href) {\n    return (\n      <motion.a\n        className={cn(sharedClass, \"group flex items-center gap-1.5\")}\n        href={href}\n        onClick={onClick}\n        whileTap={whileTap}\n      >\n        {label}\n        <ArrowRight className=\"size-3 transition-transform duration-200 group-hover:translate-x-0.5\" />\n      </motion.a>\n    );\n  }\n\n  return (\n    <motion.button\n      className={sharedClass}\n      onClick={onClick}\n      type=\"button\"\n      whileTap={whileTap}\n    >\n      {label}\n    </motion.button>\n  );\n}\n\nfunction BannerDismiss({\n  dismissClass,\n  onClick,\n  reducedMotion,\n}: {\n  dismissClass: string;\n  onClick: () => void;\n  reducedMotion: boolean;\n}) {\n  return (\n    <motion.button\n      aria-label=\"Dismiss banner\"\n      className={cn(\n        \"flex size-7 shrink-0 items-center justify-center rounded-md outline-none transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-ring/60\",\n        dismissClass\n      )}\n      onClick={onClick}\n      type=\"button\"\n      whileTap={reducedMotion ? undefined : { scale: 0.96 }}\n    >\n      <X className=\"size-4\" />\n    </motion.button>\n  );\n}\n\n/**\n * Bar layout: ringed icon and message on the left, outlined action and\n * dismiss pinned to the right end.\n */\nfunction BannerBarContent({\n  children,\n  leadingIcon,\n  style,\n  actionLabel,\n  actionHref,\n  onActionClick,\n  dismissible,\n  onClose,\n  reducedMotion,\n}: {\n  children: ReactNode;\n  leadingIcon: ReactNode;\n  style: VariantStyle;\n  actionLabel?: string;\n  actionHref?: string;\n  onActionClick: () => void;\n  dismissible: boolean;\n  onClose: () => void;\n  reducedMotion: boolean;\n}) {\n  return (\n    <>\n      {leadingIcon ? (\n        <span\n          aria-hidden\n          className={cn(\n            \"flex size-8 shrink-0 items-center justify-center rounded-full border\",\n            style.ring\n          )}\n        >\n          {leadingIcon}\n        </span>\n      ) : null}\n\n      <span className=\"min-w-0 flex-1 text-left font-medium leading-snug\">\n        {children}\n      </span>\n\n      {actionLabel ? (\n        <BannerAction\n          actionClass={style.action}\n          href={actionHref}\n          label={actionLabel}\n          onClick={onActionClick}\n          reducedMotion={reducedMotion}\n        />\n      ) : null}\n\n      {dismissible ? (\n        <BannerDismiss\n          dismissClass={style.dismiss}\n          onClick={onClose}\n          reducedMotion={reducedMotion}\n        />\n      ) : null}\n    </>\n  );\n}\n\nfunction BannerPillContent({\n  icon,\n  iconClass,\n  message,\n}: {\n  icon: ReactNode;\n  iconClass: string;\n  message?: string;\n}) {\n  return (\n    <>\n      <span className={cn(\"shrink-0\", iconClass)}>\n        {icon ?? <Check className=\"size-4\" strokeWidth={2.5} />}\n      </span>\n      <span className=\"whitespace-nowrap\">{message}</span>\n    </>\n  );\n}\n\n/**\n * The morphing surface: a full-width bar that springs into a centered\n * confirmation pill via a single layout animation while its content\n * crossfades through a soft blur.\n */\nfunction BannerSurface({\n  children,\n  isPill,\n  style,\n  leadingIcon,\n  actionLabel,\n  actionHref,\n  onActionClick,\n  morphIcon,\n  morphMessage,\n  dismissible,\n  onClose,\n  reducedMotion,\n}: {\n  children: ReactNode;\n  isPill: boolean;\n  style: VariantStyle;\n  leadingIcon: ReactNode;\n  actionLabel?: string;\n  actionHref?: string;\n  onActionClick: () => void;\n  morphIcon?: ReactNode;\n  morphMessage?: string;\n  dismissible: boolean;\n  onClose: () => void;\n  reducedMotion: boolean;\n}) {\n  const morphTransition = reducedMotion ? { duration: 0 } : MORPH_SPRING;\n\n  return (\n    <div className={cn(\"flex justify-center\", isPill && \"py-2.5\")}>\n      <motion.div\n        className={cn(\n          \"relative overflow-hidden border text-sm\",\n          isPill ? \"w-auto border shadow-lg\" : \"w-full border-x-0 border-t-0\",\n          style.surface\n        )}\n        data-phase={isPill ? \"pill\" : \"bar\"}\n        layout={!reducedMotion}\n        style={{ borderRadius: isPill ? 9999 : 0 }}\n        transition={morphTransition}\n      >\n        {reducedMotion || isPill ? null : <BannerSheen />}\n\n        <AnimatePresence initial={false} mode=\"popLayout\">\n          {isPill ? (\n            <motion.div\n              animate=\"animate\"\n              className=\"flex items-center gap-2 px-4 py-2 font-medium\"\n              exit=\"exit\"\n              initial=\"initial\"\n              key=\"pill\"\n              layout=\"position\"\n              role=\"status\"\n              transition={morphTransition}\n              variants={contentVariants}\n            >\n              <BannerPillContent\n                icon={morphIcon}\n                iconClass={style.ring}\n                message={morphMessage}\n              />\n            </motion.div>\n          ) : (\n            <motion.div\n              animate=\"animate\"\n              className=\"flex items-center gap-3 px-4 py-2.5 sm:px-6\"\n              exit=\"exit\"\n              initial=\"initial\"\n              key=\"bar\"\n              layout=\"position\"\n              transition={morphTransition}\n              variants={contentVariants}\n            >\n              <BannerBarContent\n                actionHref={actionHref}\n                actionLabel={actionLabel}\n                dismissible={dismissible}\n                leadingIcon={leadingIcon}\n                onActionClick={onActionClick}\n                onClose={onClose}\n                reducedMotion={reducedMotion}\n                style={style}\n              >\n                {children}\n              </BannerBarContent>\n            </motion.div>\n          )}\n        </AnimatePresence>\n      </motion.div>\n    </div>\n  );\n}\n\nexport type BannerProps = {\n  /** Announcement message, left-aligned next to the icon. */\n  children: ReactNode;\n  /** Visual tone of the gradient surface. */\n  variant?: BannerVariant;\n  /** Icon inside the leading ring. Each variant ships a default; pass null to hide. */\n  icon?: ReactNode;\n  /** Label for the outlined action button on the right end. */\n  actionLabel?: string;\n  /** Renders the action as a link and appends an arrow that nudges on hover. */\n  actionHref?: string;\n  /** Called when the action is clicked. */\n  onAction?: () => void;\n  /**\n   * Enables the fluid morph: after the action is clicked the full-width bar\n   * melts into a centered confirmation pill showing this message.\n   */\n  morphMessage?: string;\n  /** Icon inside the confirmation pill. Defaults to a check. */\n  morphIcon?: ReactNode;\n  /**\n   * How long the confirmation pill stays before the banner dismisses itself,\n   * in milliseconds. Pass 0 to keep the pill on screen.\n   */\n  morphDuration?: number;\n  /** Show the dismiss button on the right end. */\n  dismissible?: boolean;\n  /** Called after the banner is dismissed — by the X or after a morph. */\n  onDismiss?: () => void;\n  /** Controlled visibility. The banner animates out when this turns false. */\n  open?: boolean;\n  /** Pin the banner to the top of the viewport instead of rendering in flow. */\n  fixed?: boolean;\n  /** Extra classes for the positioning wrapper. */\n  className?: string;\n};\n\nexport function Banner({\n  children,\n  variant = \"default\",\n  icon,\n  actionLabel,\n  actionHref,\n  onAction,\n  morphMessage,\n  morphIcon,\n  morphDuration = DEFAULT_MORPH_DURATION_MS,\n  dismissible = true,\n  onDismiss,\n  open,\n  fixed = false,\n  className,\n}: BannerProps) {\n  const reducedMotion = useReducedMotion() ?? false;\n  const [internalOpen, setInternalOpen] = useState(true);\n  const [phase, setPhase] = useState<\"bar\" | \"pill\">(\"bar\");\n  const dismissTimer = useRef<number | null>(null);\n\n  const isOpen = open ?? internalOpen;\n  const style = VARIANT_STYLES[variant];\n  const leadingIcon = icon === undefined ? VARIANT_ICONS[variant] : icon;\n\n  const close = useCallback(() => {\n    setInternalOpen(false);\n    onDismiss?.();\n  }, [onDismiss]);\n\n  const handleAction = useCallback(() => {\n    onAction?.();\n\n    if (morphMessage) {\n      setPhase(\"pill\");\n    }\n  }, [onAction, morphMessage]);\n\n  // The pill lingers for morphDuration, then the banner dismisses itself.\n  useEffect(() => {\n    if (phase !== \"pill\" || morphDuration <= 0) {\n      return;\n    }\n\n    dismissTimer.current = window.setTimeout(close, morphDuration);\n\n    return () => {\n      if (dismissTimer.current !== null) {\n        window.clearTimeout(dismissTimer.current);\n      }\n    };\n  }, [phase, morphDuration, close]);\n\n  return (\n    <div\n      className={cn(\n        fixed ? \"fixed inset-x-0 top-0 z-50\" : \"relative w-full\",\n        className\n      )}\n      data-slot=\"banner\"\n      data-variant={variant}\n    >\n      <AnimatePresence>\n        {isOpen ? (\n          <motion.div\n            animate={{ height: \"auto\", opacity: 1 }}\n            className=\"overflow-hidden\"\n            exit={{ height: 0, opacity: 0 }}\n            initial={reducedMotion ? false : { height: 0, opacity: 0 }}\n            transition={reducedMotion ? { duration: 0 } : COLLAPSE_TRANSITION}\n          >\n            <BannerSurface\n              actionHref={actionHref}\n              actionLabel={actionLabel}\n              dismissible={dismissible}\n              isPill={phase === \"pill\"}\n              leadingIcon={leadingIcon}\n              morphIcon={morphIcon}\n              morphMessage={morphMessage}\n              onActionClick={handleAction}\n              onClose={close}\n              reducedMotion={reducedMotion}\n              style={style}\n            >\n              {children}\n            </BannerSurface>\n          </motion.div>\n        ) : null}\n      </AnimatePresence>\n    </div>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "title": "Banner",
  "description": "Top-of-screen announcement banner with four gradient tones, a left-aligned ringed icon and message, a right-aligned outlined action, a dismiss collapse, and a fluid spring morph that melts the bar into a confirmation pill when its action is clicked."
}
