{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "announcement-bar",
  "type": "registry:ui",
  "title": "Announcement bar",
  "description": "A top banner that rotates messages, counts down, and collapses smoothly when dismissed.",
  "categories": [
    "feedback"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "https://irisui.dev/r/foundation.json",
    "https://irisui.dev/r/motion-tokens.json",
    "https://irisui.dev/r/utils.json"
  ],
  "files": [
    {
      "path": "src/registry/components/announcement-bar/announcement-bar.tsx",
      "content": "\"use client\";\n\nimport { forwardRef, useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from \"react\";\nimport type { CSSProperties, ReactNode } from \"react\";\nimport {\n  AnimatePresence,\n  animate,\n  motion,\n  useIsPresent,\n  useMotionValue,\n  useReducedMotion,\n  useTransform,\n} from \"motion/react\";\nimport type { AnimationPlaybackControls, Transition, Variants } from \"motion/react\";\nimport { ArrowRight, ChevronDown, ChevronUp, Pause, Play, X } from \"lucide-react\";\nimport { motionTokens } from \"../lib/motion-tokens\";\nimport { cn } from \"../lib/utils\";\n\nexport interface AnnouncementAction {\n  label: string;\n  href?: string;\n  onClick?: () => void;\n}\n\nexport interface Announcement {\n  /** Stable id, reported to callbacks. */\n  id: string;\n  message: ReactNode;\n  action?: AnnouncementAction;\n  /** Counts down to a moment, for example the end of a sale. */\n  countdown?: { to: Date | string | number; label?: string };\n}\n\n/**\n * A slim bar for the top of a page. It can rotate several messages, each rising in from below while the bar springs to\n * the new height; rotation pauses on hover, focus, or a hidden tab, and `controls` adds previous, next, and a pause ring. Messages can carry a\n * call to action and a live countdown. Dismissing collapses the height smoothly so the page below eases up, and with an\n * `id` the dismissal is remembered in `localStorage`.\n */\nexport interface AnnouncementBarProps {\n  messages: Announcement[];\n  /** Remembers dismissal under this id. Change the id to show a new campaign to everyone again. */\n  id?: string;\n  /** Whether the bar is shown. Leave it out to let the component manage it. */\n  open?: boolean;\n  defaultOpen?: boolean;\n  onOpenChange?: (open: boolean) => void;\n  /** Index of the visible message. */\n  index?: number;\n  defaultIndex?: number;\n  onIndexChange?: (index: number) => void;\n  /** Milliseconds each message stays before the next one. Defaults to 6000. */\n  interval?: number;\n  /** Rotate automatically. Pauses on hover, focus, or a hidden tab, and is turned off when the visitor prefers reduced motion. Defaults to true. */\n  autoPlay?: boolean;\n  /** Show previous, next, and pause controls when there are several messages. Defaults to false: only the close button shows. */\n  controls?: boolean;\n  dismissible?: boolean;\n  tone?: \"neutral\" | \"inverted\";\n  onAction?: (announcement: Announcement) => void;\n  onCountdownEnd?: (announcement: Announcement) => void;\n  /** Accessible name of the region. */\n  label?: string;\n  className?: string;\n}\n\nconst storageKey = (id: string) => `iris-announcement:${id}`;\n/** Forgets a remembered dismissal so the bar with this id shows again. */\nexport function clearAnnouncementDismissal(id: string) {\n  try {\n    window.localStorage.removeItem(storageKey(id));\n  } catch {\n    /* Storage can be blocked. */\n  }\n}\n\ntype Bezier = [number, number, number, number];\nconst enter = [...motionTokens.ease.enter] as Bezier;\nconst standard = [...motionTokens.ease.standard] as Bezier;\nconst physical = (visualDuration: number, bounce: number): Transition => {\n  const root = (2 * Math.PI) / (visualDuration * 1.2);\n  return { type: \"spring\", stiffness: root * root, damping: 2 * (1 - bounce) * root, mass: 1 };\n};\nconst RISE = physical(0.46, 0),\n  HEIGHT = physical(0.42, 0),\n  COLLAPSE = physical(0.44, 0);\n\n/** Next rises from below and leaves upward; previous runs the other way. */\nconst faceVariants: Variants = {\n  hidden: (direction: number) => ({ opacity: 0, y: `${direction * 70}%`, filter: `blur(${motionTokens.blur.soft}px)` }),\n  shown: {\n    opacity: 1,\n    y: \"0%\",\n    filter: \"blur(0px)\",\n    transition: {\n      y: RISE,\n      opacity: { duration: motionTokens.duration.standard, ease: enter },\n      filter: { duration: motionTokens.duration.standard, ease: enter },\n    },\n  },\n  gone: (direction: number) => ({\n    opacity: 0,\n    y: `${direction * -60}%`,\n    filter: `blur(${motionTokens.blur.soft}px)`,\n    transition: {\n      y: RISE,\n      opacity: { duration: motionTokens.duration.fast, ease: standard },\n      filter: { duration: motionTokens.duration.fast, ease: standard },\n    },\n  }),\n};\n/* Reduced motion only fades. Its states also pin y and filter at rest: the server renders without knowing the setting, so a\n   shown face must carry the same styles in both branches to hydrate cleanly. */\nconst fadeVariants: Variants = {\n  hidden: { opacity: 0, y: \"0%\", filter: \"blur(0px)\" },\n  shown: {\n    opacity: 1,\n    y: \"0%\",\n    filter: \"blur(0px)\",\n    transition: { duration: motionTokens.duration.standard, ease: standard },\n  },\n  gone: {\n    opacity: 0,\n    y: \"0%\",\n    filter: \"blur(0px)\",\n    transition: { duration: motionTokens.duration.instant, ease: standard },\n  },\n};\n\nfunction parts(ms: number) {\n  const total = Math.max(0, Math.floor(ms / 1000));\n  return {\n    d: Math.floor(total / 86400),\n    h: Math.floor((total % 86400) / 3600),\n    m: Math.floor((total % 3600) / 60),\n    s: total % 60,\n  };\n}\nconst pad = (value: number) => String(value).padStart(2, \"0\");\n\n/**\n * One character cell. A changed digit drops in from above while the old one falls away, like a counter winding down.\n * The invisible sizer keeps the cell in the text flow, so it shares the baseline of the words around it.\n */\nfunction Digit({ char, reduced }: { char: string; reduced: boolean }) {\n  return (\n    <span className=\"relative inline-block overflow-clip text-center\">\n      <span className=\"invisible\" aria-hidden=\"true\">\n        0\n      </span>\n      <AnimatePresence initial={false}>\n        <motion.span\n          key={char}\n          className=\"absolute inset-0 block\"\n          initial={reduced ? { opacity: 0 } : { y: \"-70%\", opacity: 0 }}\n          animate={{ y: \"0%\", opacity: 1 }}\n          exit={reduced ? { opacity: 0 } : { y: \"70%\", opacity: 0 }}\n          transition={\n            reduced\n              ? { duration: motionTokens.duration.instant }\n              : { y: physical(0.32, 0), opacity: { duration: motionTokens.duration.fast, ease: standard } }\n          }\n        >\n          {char}\n        </motion.span>\n      </AnimatePresence>\n    </span>\n  );\n}\n\nfunction Countdown({\n  to,\n  label,\n  reduced,\n  onEnd,\n}: {\n  to: Date | string | number;\n  label?: string;\n  reduced: boolean;\n  onEnd?: () => void;\n}) {\n  const target = new Date(to).getTime();\n  const [now, setNow] = useState<number | null>(null);\n  const ended = useRef(false);\n  const endRef = useRef(onEnd);\n  useEffect(() => {\n    endRef.current = onEnd;\n  }, [onEnd]);\n  useEffect(() => {\n    let timer = 0;\n    const tick = () => {\n      const current = Date.now();\n      setNow(current);\n      if (current >= target) {\n        if (!ended.current) {\n          ended.current = true;\n          endRef.current?.();\n        }\n        return;\n      }\n      // Wake just after each whole second so digits change on the beat.\n      timer = window.setTimeout(tick, 1000 - (current % 1000) + 8);\n    };\n    timer = window.setTimeout(tick, 0);\n    return () => window.clearTimeout(timer);\n  }, [target]);\n  const { d, h, m, s } = parts(now === null ? 0 : target - now);\n  const pending = now === null;\n  const spoken = pending ? \"\" : `${d ? `${d} days ` : \"\"}${h} hours ${m} minutes`;\n  const digits = (value: string, key: string) =>\n    value\n      .split(\"\")\n      .map((char, index) => (\n        <Digit key={`${key}${value.length - index}`} char={pending ? \"0\" : char} reduced={reduced} />\n      ));\n  return (\n    <span className=\"inline-flex items-baseline gap-1.5 whitespace-nowrap text-(color:--muted)\">\n      {label ? <span className=\"text-inherit\">{label}</span> : null}\n      <span\n        role=\"timer\"\n        aria-live=\"off\"\n        aria-label={spoken}\n        className=\"inline-flex items-baseline leading-[inherit] font-medium text-ink tabular-nums group-data-[tone=inverted]/bar:text-page data-pending:opacity-0\"\n        data-pending={pending || undefined}\n      >\n        {d ? (\n          <>\n            <span className=\"inline-flex items-baseline\">\n              {digits(String(d), \"d\")}\n              <span className=\"ml-px font-normal text-(color:--muted)\" aria-hidden=\"true\">\n                d\n              </span>\n            </span>\n            <span className=\"inline-block w-[.4em]\" aria-hidden=\"true\" />\n          </>\n        ) : null}\n        {digits(pad(h), \"h\")}\n        <span\n          className=\"inline-block px-px font-normal text-(color:--muted) [font-feature-settings:'case']\"\n          aria-hidden=\"true\"\n        >\n          :\n        </span>\n        {digits(pad(m), \"m\")}\n        <span\n          className=\"inline-block px-px font-normal text-(color:--muted) [font-feature-settings:'case']\"\n          aria-hidden=\"true\"\n        >\n          :\n        </span>\n        {digits(pad(s), \"s\")}\n      </span>\n    </span>\n  );\n}\n\nfunction Face({\n  announcement,\n  direction,\n  reduced,\n  position,\n  total,\n  onSize,\n  onAction,\n  onCountdownEnd,\n}: {\n  announcement: Announcement;\n  direction: number;\n  reduced: boolean;\n  position: number;\n  total: number;\n  onSize: (height: number) => void;\n  onAction?: (announcement: Announcement) => void;\n  onCountdownEnd?: (announcement: Announcement) => void;\n}) {\n  const ref = useRef<HTMLDivElement>(null);\n  const present = useIsPresent();\n  useLayoutEffect(() => {\n    const node = ref.current;\n    if (!node || !present) return;\n    const report = () => onSize(node.offsetHeight);\n    report();\n    if (typeof ResizeObserver === \"undefined\") return;\n    const observer = new ResizeObserver(report);\n    observer.observe(node);\n    return () => observer.disconnect();\n  }, [onSize, present]);\n  const { action, countdown } = announcement;\n  const cta = action ? (\n    action.href ? (\n      <a\n        className=\"inline-flex cursor-pointer items-center gap-1 border-0 bg-none p-0 font-medium whitespace-nowrap text-inherit no-underline [-webkit-tap-highlight-color:transparent] [&_svg]:transition-transform [&_svg]:duration-fast [&_svg]:ease-standard hover:[&_svg]:translate-x-0.5 motion-reduce:[&_svg]:transition-none\"\n        href={action.href}\n        onClick={() => {\n          action.onClick?.();\n          onAction?.(announcement);\n        }}\n      >\n        {action.label}\n        <ArrowRight size={14} strokeWidth={1.75} aria-hidden=\"true\" />\n      </a>\n    ) : (\n      <button\n        type=\"button\"\n        className=\"inline-flex cursor-pointer items-center gap-1 border-0 bg-none p-0 font-medium whitespace-nowrap text-inherit no-underline [-webkit-tap-highlight-color:transparent] [&_svg]:transition-transform [&_svg]:duration-fast [&_svg]:ease-standard hover:[&_svg]:translate-x-0.5 motion-reduce:[&_svg]:transition-none\"\n        onClick={() => {\n          action.onClick?.();\n          onAction?.(announcement);\n        }}\n      >\n        {action.label}\n        <ArrowRight size={14} strokeWidth={1.75} aria-hidden=\"true\" />\n      </button>\n    )\n  ) : null;\n  return (\n    <motion.div\n      ref={ref}\n      className=\"absolute top-0 right-0 left-0 flex min-h-8 items-center justify-center @max-[560px]:justify-start\"\n      custom={direction}\n      variants={reduced ? fadeVariants : faceVariants}\n      initial=\"hidden\"\n      animate=\"shown\"\n      exit=\"gone\"\n      role={total > 1 ? \"group\" : undefined}\n      aria-roledescription={total > 1 ? \"slide\" : undefined}\n      aria-label={total > 1 ? `${position} of ${total}` : undefined}\n      inert={!present || undefined}\n    >\n      <p className=\"m-0 flex flex-wrap items-baseline justify-center gap-x-3 gap-y-0.5 px-0 py-1.5 text-center text-(length:--text-sm) leading-body text-balance @max-[560px]:justify-start @max-[560px]:gap-x-2.5 @max-[560px]:text-left\">\n        <span className=\"min-w-0\">{announcement.message}</span>\n        {countdown ? (\n          <Countdown\n            to={countdown.to}\n            label={countdown.label}\n            reduced={reduced}\n            onEnd={() => onCountdownEnd?.(announcement)}\n          />\n        ) : null}\n        {cta}\n      </p>\n    </motion.div>\n  );\n}\n\nconst RING = 2 * Math.PI * 8;\n\nexport const AnnouncementBar = forwardRef<HTMLElement, AnnouncementBarProps>(function AnnouncementBar(\n  {\n    messages,\n    id,\n    open: openProp,\n    defaultOpen = true,\n    onOpenChange,\n    index: indexProp,\n    defaultIndex = 0,\n    onIndexChange,\n    interval = 6000,\n    autoPlay = true,\n    controls = false,\n    dismissible = true,\n    tone = \"neutral\",\n    onAction,\n    onCountdownEnd,\n    label = \"Announcements\",\n    className,\n  },\n  ref,\n) {\n  const reduced = !!useReducedMotion();\n  const uid = useId();\n  const total = messages.length;\n\n  /* Open state: controlled, or internal and hidden at once when this id was dismissed before. */\n  const [openInternal, setOpenInternal] = useState(defaultOpen);\n  const [remembered, setRemembered] = useState(false);\n  useLayoutEffect(() => {\n    if (!id || openProp !== undefined) return;\n    let stored = false;\n    try {\n      stored = window.localStorage.getItem(storageKey(id)) === \"dismissed\";\n    } catch {\n      /* Storage can be blocked. */\n    }\n    // Storage is only readable on the client; hide before paint so a dismissed bar never flashes.\n    // eslint-disable-next-line react-hooks/set-state-in-effect\n    if (stored) setRemembered(true);\n  }, [id, openProp]);\n  const open = openProp ?? (openInternal && !remembered);\n\n  const dismiss = () => {\n    if (id) {\n      try {\n        window.localStorage.setItem(storageKey(id), \"dismissed\");\n      } catch {\n        /* Storage can be blocked. */\n      }\n    }\n    if (openProp === undefined) setOpenInternal(false);\n    onOpenChange?.(false);\n  };\n\n  /* Rotation. */\n  const [indexInternal, setIndexInternal] = useState(defaultIndex);\n  const current = total ? (((indexProp ?? indexInternal) % total) + total) % total : 0;\n  const [direction, setDirection] = useState(1);\n  const go = useCallback(\n    (step: number) => {\n      if (total < 2) return;\n      const next = (((current + step) % total) + total) % total;\n      setDirection(step > 0 ? 1 : -1);\n      if (indexProp === undefined) setIndexInternal(next);\n      onIndexChange?.(next);\n    },\n    [current, indexProp, onIndexChange, total],\n  );\n\n  const [userPaused, setUserPaused] = useState(false);\n  const [hovered, setHovered] = useState(false);\n  const [focused, setFocused] = useState(false);\n  const [hidden, setHidden] = useState(false);\n  useEffect(() => {\n    const read = () => setHidden(document.visibilityState === \"hidden\");\n    document.addEventListener(\"visibilitychange\", read);\n    return () => document.removeEventListener(\"visibilitychange\", read);\n  }, []);\n  const rotating = autoPlay && !reduced && total > 1;\n  const running = rotating && open && !userPaused && !hovered && !focused && !hidden;\n\n  /* The ring is the clock: its progress runs to one, then advances. Pausing stops it where it is; resuming finishes the rest. */\n  const progress = useMotionValue(0);\n  const dash = useTransform(progress, (value) => RING * (1 - value));\n  const goRef = useRef(go);\n  useEffect(() => {\n    goRef.current = go;\n  }, [go]);\n  useEffect(() => {\n    progress.jump(0);\n  }, [current, progress]);\n  useEffect(() => {\n    if (!running) return;\n    let controls: AnimationPlaybackControls | null = animate(progress, 1, {\n      duration: (interval / 1000) * (1 - progress.get()),\n      ease: \"linear\",\n      onComplete: () => {\n        controls = null;\n        goRef.current(1);\n      },\n    });\n    return () => controls?.stop();\n  }, [running, current, interval, progress]);\n\n  /* The viewport springs to the height of the current message, which can wrap on narrow screens. */\n  const height = useMotionValue<number | \"auto\">(\"auto\");\n  const measured = useRef(0);\n  const onSize = useCallback(\n    (next: number) => {\n      if (Math.abs(next - measured.current) < 0.5) return;\n      const first = measured.current === 0;\n      measured.current = next;\n      if (first || reduced) height.jump(next);\n      else animate(height, next, HEIGHT);\n    },\n    [height, reduced],\n  );\n\n  const announcement = messages[current];\n  if (!announcement) return null;\n  const navigable = controls && total > 1;\n  const controlCount = (navigable ? 2 + (rotating ? 1 : 0) : 0) + (dismissible ? 1 : 0);\n\n  return (\n    <AnimatePresence initial={false}>\n      {open ? (\n        <motion.section\n          key=\"bar\"\n          ref={ref}\n          className={cn(\"@container overflow-hidden font-body tracking-body\", className)}\n          aria-label={label}\n          aria-roledescription={total > 1 ? \"carousel\" : undefined}\n          initial={reduced ? { opacity: 0 } : { height: 0, opacity: 0 }}\n          animate={{ height: \"auto\", opacity: 1 }}\n          exit={\n            reduced\n              ? { opacity: 0, transition: { duration: motionTokens.duration.fast } }\n              : {\n                  height: 0,\n                  opacity: 0,\n                  transition: { height: COLLAPSE, opacity: { duration: motionTokens.duration.exit, ease: standard } },\n                }\n          }\n          transition={\n            reduced\n              ? { duration: motionTokens.duration.fast }\n              : { height: COLLAPSE, opacity: { duration: motionTokens.duration.standard, ease: enter } }\n          }\n          onPointerEnter={(event) => {\n            if (event.pointerType === \"mouse\") setHovered(true);\n          }}\n          onPointerLeave={() => setHovered(false)}\n          onFocus={() => setFocused(true)}\n          onBlur={(event) => {\n            if (!event.currentTarget.contains(event.relatedTarget as Node | null)) setFocused(false);\n          }}\n        >\n          <div\n            className=\"group/bar grid grid-cols-[calc(var(--controls,0)_*_var(--icon)_+_max(var(--controls,0)_-_1,0)_*_var(--control-gap))_minmax(0,1fr)_auto] items-center gap-3 border-b border-line bg-hover py-1.5 pr-2 pl-4 text-ink [--control-gap:2px] [--icon:32px] [--muted:var(--ink-2)] data-[tone=inverted]:border-b-transparent data-[tone=inverted]:bg-ink data-[tone=inverted]:text-page data-[tone=inverted]:[--muted:color-mix(in_oklab,var(--page)_66%,transparent)] @max-[560px]:grid-cols-[0_minmax(0,1fr)_auto] @max-[560px]:gap-x-2 @max-[560px]:gap-y-0 @max-[560px]:pl-3.5\"\n            data-tone={tone}\n            style={{ \"--controls\": controlCount } as CSSProperties}\n          >\n            <div className=\"min-w-0\" aria-hidden=\"true\" />\n            <motion.div\n              className=\"relative min-h-8 overflow-hidden\"\n              style={{ height }}\n              id={`${uid}-slides`}\n              aria-live={running ? \"off\" : \"polite\"}\n              aria-atomic=\"false\"\n            >\n              <AnimatePresence initial={false} custom={direction}>\n                <Face\n                  key={announcement.id}\n                  announcement={announcement}\n                  direction={direction}\n                  reduced={reduced}\n                  position={current + 1}\n                  total={total}\n                  onSize={onSize}\n                  onAction={onAction}\n                  onCountdownEnd={onCountdownEnd}\n                />\n              </AnimatePresence>\n            </motion.div>\n            <div className=\"flex items-center gap-(--control-gap)\">\n              {navigable ? (\n                <>\n                  <button\n                    type=\"button\"\n                    className=\"relative grid size-(--icon) flex-none cursor-pointer place-items-center rounded-button border-0 bg-transparent p-0 text-(color:--muted) [-webkit-tap-highlight-color:transparent] transition-[color,background-color] duration-fast ease-standard hover:bg-[color-mix(in_oklab,currentColor_10%,transparent)] hover:text-ink group-data-[tone=inverted]/bar:hover:text-page active:bg-[color-mix(in_oklab,currentColor_12%,transparent)] active:text-inherit motion-reduce:transition-none\"\n                    aria-label=\"Previous announcement\"\n                    aria-controls={`${uid}-slides`}\n                    onClick={() => go(-1)}\n                  >\n                    <ChevronUp size={16} strokeWidth={1.75} aria-hidden=\"true\" />\n                  </button>\n                  {rotating ? (\n                    <button\n                      type=\"button\"\n                      className=\"relative grid size-(--icon) flex-none cursor-pointer place-items-center rounded-button border-0 bg-transparent p-0 text-(color:--muted) [-webkit-tap-highlight-color:transparent] transition-[color,background-color] duration-fast ease-standard hover:bg-[color-mix(in_oklab,currentColor_10%,transparent)] hover:text-ink group-data-[tone=inverted]/bar:hover:text-page active:bg-[color-mix(in_oklab,currentColor_12%,transparent)] active:text-inherit motion-reduce:transition-none\"\n                      aria-label={userPaused ? \"Resume announcements\" : \"Pause announcements\"}\n                      aria-pressed={userPaused}\n                      onClick={() => setUserPaused((paused) => !paused)}\n                    >\n                      <svg viewBox=\"0 0 20 20\" className=\"size-5 -rotate-90 overflow-visible\" aria-hidden=\"true\">\n                        <circle\n                          cx=\"10\"\n                          cy=\"10\"\n                          r=\"8\"\n                          className=\"fill-none stroke-current stroke-[1.75] [stroke-opacity:.22]\"\n                        />\n                        <motion.circle\n                          cx=\"10\"\n                          cy=\"10\"\n                          r=\"8\"\n                          className=\"fill-none stroke-current stroke-[1.75] [stroke-linecap:round]\"\n                          strokeDasharray={RING}\n                          style={{ strokeDashoffset: dash }}\n                        />\n                      </svg>\n                      <span className=\"absolute inset-0 grid place-items-center\">\n                        {userPaused ? (\n                          <Play size={9} strokeWidth={2.4} className=\"size-[9px] fill-current\" aria-hidden=\"true\" />\n                        ) : (\n                          <Pause size={9} strokeWidth={2.4} className=\"size-[9px] fill-current\" aria-hidden=\"true\" />\n                        )}\n                      </span>\n                    </button>\n                  ) : null}\n                  <button\n                    type=\"button\"\n                    className=\"relative grid size-(--icon) flex-none cursor-pointer place-items-center rounded-button border-0 bg-transparent p-0 text-(color:--muted) [-webkit-tap-highlight-color:transparent] transition-[color,background-color] duration-fast ease-standard hover:bg-[color-mix(in_oklab,currentColor_10%,transparent)] hover:text-ink group-data-[tone=inverted]/bar:hover:text-page active:bg-[color-mix(in_oklab,currentColor_12%,transparent)] active:text-inherit motion-reduce:transition-none\"\n                    aria-label=\"Next announcement\"\n                    aria-controls={`${uid}-slides`}\n                    onClick={() => go(1)}\n                  >\n                    <ChevronDown size={16} strokeWidth={1.75} aria-hidden=\"true\" />\n                  </button>\n                </>\n              ) : null}\n              {dismissible ? (\n                <button\n                  type=\"button\"\n                  className=\"relative grid size-(--icon) flex-none cursor-pointer place-items-center rounded-button border-0 bg-transparent p-0 text-(color:--muted) [-webkit-tap-highlight-color:transparent] transition-[color,background-color] duration-fast ease-standard hover:bg-[color-mix(in_oklab,currentColor_10%,transparent)] hover:text-ink group-data-[tone=inverted]/bar:hover:text-page active:bg-[color-mix(in_oklab,currentColor_12%,transparent)] active:text-inherit motion-reduce:transition-none\"\n                  aria-label=\"Dismiss\"\n                  onClick={dismiss}\n                >\n                  <X size={16} strokeWidth={1.75} aria-hidden=\"true\" />\n                </button>\n              ) : null}\n            </div>\n          </div>\n        </motion.section>\n      ) : null}\n    </AnimatePresence>\n  );\n});\n\nexport default AnnouncementBar;\n",
      "type": "registry:component",
      "target": "@components/iris/announcement-bar/announcement-bar.tsx"
    }
  ],
  "docs": "Docs and live preview: https://irisui.dev/components/announcement-bar",
  "meta": {
    "kind": "component",
    "group": "Feedback",
    "export": "AnnouncementBar",
    "tags": [
      "feedback",
      "new"
    ],
    "docs": "https://irisui.dev/components/announcement-bar"
  }
}
