For the complete documentation index, see llms.txt. Markdown variants are available by appending .md to documentation URLs. Homepage markdown is available at /llms.md/content.md. An agent skill is available at /.well-known/agent-skills/site-skill.md.

Announcement

A compact card for sharing an update or action.

Installation

$ pnpm dlx shadcn@latest add @cobalt/announcement

Source

"use client";

import { RiSparklingFill, RiCloseLine } from "@remixicon/react";
import { cn } from "cn";
import { AnimatePresence, motion } from "motion/react";
import * as React from "react";

import { useReducedMotion } from "@/components/hooks/use-reduced-motion";
import { Button } from "@/components/ui/button";

type IconComponent = React.ComponentType<{
  className?: string;
  "aria-hidden"?: boolean | "true" | "false";
}>;

interface AnnouncementProps extends Omit<
  React.ComponentProps<"div">,
  | "title"
  | "onDrag"
  | "onDragStart"
  | "onDragEnd"
  | "onAnimationStart"
  | "onAnimationEnd"
> {
  title: React.ReactNode;
  description?: React.ReactNode;
  icon?: IconComponent;
  actionLabel?: React.ReactNode;
  onAction?: () => void;
  dismissible?: boolean;
  onClose?: () => void;
  closeLabel?: string;
  introDelay?: number;
}

function Announcement({
  title,
  description,
  icon: Icon = RiSparklingFill,
  actionLabel,
  onAction,
  dismissible = false,
  onClose,
  closeLabel = "Dismiss",
  introDelay,
  className,
  ref,
  ...props
}: AnnouncementProps) {
  const [dismissed, setDismissed] = React.useState(false);
  const reducedMotion = useReducedMotion();
  const hasIntro = introDelay !== undefined;

  return (
    <AnimatePresence onExitComplete={onClose}>
      {!dismissed && (
        <motion.div
          ref={ref}
          data-slot="announcement"
          initial={
            hasIntro
              ? { opacity: 0, y: 15, scale: 0.85, filter: "blur(6px)" }
              : false
          }
          animate={
            hasIntro
              ? {
                  opacity: 1,
                  y: 0,
                  scale: 1,
                  filter: "blur(0px)",
                  transition: {
                    duration: reducedMotion ? 0 : 0.25,
                    ease: "easeOut",
                    delay: introDelay,
                  },
                }
              : undefined
          }
          exit={
            reducedMotion
              ? { opacity: 0, transition: { duration: 0 } }
              : {
                  opacity: 0,
                  scale: 0.85,
                  filter: "blur(6px)",
                  transition: { duration: 0.25, ease: "easeInOut" },
                }
          }
          className={cn(
            "relative flex w-full flex-col items-start gap-3 rounded-[12px] border border-[var(--border-subtle,var(--border))] bg-popover p-3",
            className
          )}
          {...props}
        >
          <div className="flex w-full flex-col items-start gap-1">
            <Icon
              className="size-5 shrink-0 text-[color:var(--foreground-secondary,var(--muted-foreground))]"
              aria-hidden
            />
            {dismissible && (
              <Button
                variant="close"
                size="icon-2xs"
                aria-label={closeLabel}
                onClick={() => setDismissed(true)}
                className="absolute end-3 top-3"
              >
                <RiCloseLine />
              </Button>
            )}
            <div className="flex w-full flex-col items-start gap-0.5">
              <p className="w-full text-sm leading-5 font-medium tracking-normal text-foreground">
                {title}
              </p>
              {description && (
                <p className="w-full text-[13px] leading-[18px] font-medium tracking-normal text-[color:var(--foreground-secondary,var(--muted-foreground))]">
                  {description}
                </p>
              )}
            </div>
          </div>
          {actionLabel && (
            <Button
              variant="outline"
              size="sm"
              onClick={onAction}
              className="w-full"
            >
              {actionLabel}
            </Button>
          )}
        </motion.div>
      )}
    </AnimatePresence>
  );
}

export { Announcement };
export type { AnnouncementProps };