{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "type": "registry:ui",
  "slug": "toaster",
  "name": "Toaster",
  "title": "Toaster",
  "description": "Layered notification cards with coordinated spring motion, synchronized countdowns, undo actions, invoice reminders and contextual footers.",
  "summary": "A controlled, theme-aware toaster inspired by layered notification cards. Includes six interactive examples, interruptible spring reflow, countdowns that pause on hover or focus, asynchronous actions and accessible dismissal.",
  "category": "feedback",
  "tags": [
    "toast",
    "toaster",
    "notification",
    "gsap",
    "spring",
    "countdown",
    "undo",
    "feedback"
  ],
  "dependencies": [
    "gsap",
    "@gsap/react",
    "motion",
    "lucide-react",
    "clsx",
    "tailwind-merge"
  ],
  "version": "1.0.0",
  "createdDate": "2026-10-19",
  "updatedDate": "2026-10-19",
  "interactive": true,
  "supportsColor": false,
  "highlights": [
    "Six notification compositions with inherited light and dark themes",
    "GSAP playback using the existing spring solver; adjustable stiffness and damping",
    "One clock synchronizes the countdown label and progress indicator",
    "Pause timers on hover, keyboard focus, hidden tabs and pending actions",
    "Actions receive AbortSignal, prevent duplicate clicks and surface recoverable errors",
    "Animated exits followed by interruptible position changes for the remaining cards",
    "Transparent card-only gallery with replay and timing controls in Customize",
    "Full-width invoice footer with a 44px action target and scoped GSAP hover feedback"
  ],
  "anatomy": [
    "Named notification region with a semantic ordered list",
    "Layered card surface, optional icon or calendar date, title and description",
    "Inline actions, metadata badges and contextual footer",
    "Synchronized remaining-time label and progress track"
  ],
  "physics": {
    "engine": "GSAP + spring easing",
    "description": "Transform and opacity entrances use a sampled spring. Dismissal finishes before the list measures viewport positions and animates each remaining item from its current visual position, including gallery centering shifts. Countdown and progress share one timeline.",
    "parameters": [
      {
        "label": "Stiffness",
        "value": "240 (80\u2013500)"
      },
      {
        "label": "Damping",
        "value": "28 (16\u201350)"
      },
      {
        "label": "Exit",
        "value": "180ms"
      },
      {
        "label": "Countdown",
        "value": "Per item, milliseconds; 0 keeps the notification open"
      }
    ]
  },
  "accessibility": {
    "role": "region",
    "aria": "Named notifications region, semantic list, labelled dismiss buttons, polite title announcements and action errors. Countdown seconds are not live-announced. Focus moves to another card or the region when the focused card is removed.",
    "reducedMotion": "Entrance, exit and reflow transforms apply immediately. Timers and actions remain functional.",
    "keyboard": [
      {
        "key": "Tab / Shift+Tab",
        "description": "Navigate notification actions; focus pauses the active card countdown."
      },
      {
        "key": "Enter / Space",
        "description": "Activate the focused action."
      },
      {
        "key": "Escape",
        "description": "Dismiss the focused notification and cancel its pending action."
      }
    ]
  },
  "guidelines": {
    "recommended": [
      "Save confirmations with reversible actions",
      "Invoice reminders, quota warnings and deployment summaries"
    ],
    "bestPractices": [
      "Use unique stable IDs; create a new ID when replaying a notification.",
      "Remove the matching item in onDismiss.",
      "Set duration to 0 for critical messages requiring user acknowledgement.",
      "Use action.onClick(signal) for real application work; honor the AbortSignal to cancel requests on dismissal or unmount.",
      "Set dismissOnSuccess to false for actions that open details. Supply a safe errorMessage for failed actions.",
      "Provide irreversible business deadlines on the server; this UI countdown pauses during user interaction.",
      "Do not mount the same notification in multiple live regions.",
      "Request programmatic exits with item.dismiss, then remove the item in onDismiss to preserve the exit animation."
    ]
  },
  "props": [
    {
      "name": "items",
      "type": "readonly ToastItem[]",
      "required": true,
      "description": "Controlled notifications with stable, unique IDs. Change the ID to replay a card. Each item supports title, description, tone, icon, date, badges, duration, countdownLabel, footer, action, secondaryAction and actionPlacement. Set item.dismiss to dismiss, timeout or action to request a coordinated exit programmatically before onDismiss."
    },
    {
      "name": "onDismiss",
      "type": "(id: string, reason: ToastDismissReason) => void",
      "required": true,
      "description": "Called after the exit animation. Remove the matching item from controlled state. Reasons are dismiss, timeout and action."
    },
    {
      "name": "stiffness",
      "type": "number",
      "defaultValue": "240",
      "description": "Spring tension, clamped to 80\u2013500. Used for entrances and stack reflow."
    },
    {
      "name": "damping",
      "type": "number",
      "defaultValue": "28",
      "description": "Spring friction, clamped to 16\u201350. Higher values reduce bounce."
    },
    {
      "name": "className",
      "type": "string",
      "description": "Classes for the notification list. Defaults to a vertical stack, up to 440px wide."
    },
    {
      "name": "ref",
      "type": "Ref<HTMLDivElement>",
      "description": "Receives the notification region for programmatic focus."
    }
  ],
  "usage": "\"use client\";\n\nimport { useState } from \"react\";\nimport { Toaster, type ToastItem } from \"@/components/ui/toaster\";\n\nexport function Notifications() {\n  const [items, setItems] = useState<ToastItem[]>([]);\n\n  return (\n    <div>\n      <button onClick={() => setItems((current) => [...current, {\n        id: crypto.randomUUID(),\n        title: \"Changes saved\",\n        description: \"Your preferences are up to date.\",\n        tone: \"info\",\n        duration: 5000,\n        countdownLabel: \"This message will close in\",\n      }])}>\n        Show notification\n      </button>\n      <Toaster\n        items={items}\n        onDismiss={(id) => setItems((current) => current.filter((item) => item.id !== id))}\n        stiffness={240}\n        damping={28}\n      />\n    </div>\n  );\n}",
  "files": [
    {
      "name": "toaster.tsx",
      "path": "registry/ui/toaster.tsx",
      "type": "registry:ui",
      "code": "\"use client\";\n\nimport {\n  useId,\n  useMemo,\n  useRef,\n  useState,\n  type ReactNode,\n  type Ref,\n} from \"react\";\nimport { useGSAP } from \"@gsap/react\";\nimport gsap from \"gsap\";\nimport { calcGeneratorDuration, spring } from \"motion\";\nimport { ArrowRight, Check, Info, LoaderCircle, X } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport { useMediaQuery } from \"@/lib/use-media-query\";\n\ngsap.registerPlugin(useGSAP);\n\nexport type ToastDismissReason = \"dismiss\" | \"timeout\" | \"action\";\n\nexport interface ToastAction {\n  label: string;\n  onClick: (signal: AbortSignal) => void | Promise<void>;\n  dismissOnSuccess?: boolean;\n  errorMessage?: string;\n}\n\nexport interface ToastItem {\n  id: string;\n  title: string;\n  description?: string;\n  tone?: \"neutral\" | \"info\" | \"danger\" | \"success\";\n  icon?: ReactNode;\n  date?: { month: string; day: string };\n  badges?: readonly string[];\n  duration?: number;\n  countdownLabel?: string;\n  footer?: string;\n  action?: ToastAction;\n  secondaryAction?: ToastAction;\n  actionPlacement?: \"inline\" | \"footer\";\n  dismiss?: ToastDismissReason;\n}\n\nexport interface ToasterProps {\n  items: readonly ToastItem[];\n  onDismiss: (id: string, reason: ToastDismissReason) => void;\n  stiffness?: number;\n  damping?: number;\n  className?: string;\n  ref?: Ref<HTMLDivElement>;\n}\n\nconst bounded = (value: number, fallback: number, min: number, max: number) =>\n  Number.isFinite(value) ? Math.min(max, Math.max(min, value)) : fallback;\n\nfunction useToastMotion(stiffness: number, damping: number) {\n  const reduced = useMediaQuery(\"(prefers-reduced-motion: reduce)\");\n  return useMemo(() => {\n    const response = spring({\n      keyframes: [0, 1],\n      stiffness: bounded(stiffness, 240, 80, 500),\n      damping: bounded(damping, 28, 16, 50),\n      restSpeed: 0.001,\n      restDelta: 0.001,\n    });\n    const duration = calcGeneratorDuration(response) / 1000;\n    return {\n      reduced,\n      duration: reduced ? 0 : duration,\n      ease: (progress: number) =>\n        progress === 1 ? 1 : response.next(progress * duration * 1000).value,\n    };\n  }, [stiffness, damping, reduced]);\n}\n\nconst buttonClass =\n  \"inline-flex min-h-8 cursor-pointer items-center justify-center gap-2 rounded-md px-1.5 text-[13px] font-medium outline-none hover:bg-black/5 focus-visible:ring-2 focus-visible:ring-current disabled:cursor-wait disabled:opacity-50 dark:hover:bg-white/10\";\n\nfunction ToastCard({\n  item,\n  onDismiss,\n  motion,\n}: {\n  item: ToastItem;\n  onDismiss: (\n    id: string,\n    reason: ToastDismissReason,\n    restoreFocus: boolean,\n  ) => void;\n  motion: ReturnType<typeof useToastMotion>;\n}) {\n  const titleId = useId();\n  const cardRef = useRef<HTMLDivElement>(null);\n  const secondsRef = useRef<HTMLSpanElement>(null);\n  const progressRef = useRef<HTMLDivElement>(null);\n  const timerRef = useRef<gsap.core.Timeline | null>(null);\n  const pausesRef = useRef(new Set<string>());\n  const closingRef = useRef(false);\n  const restoreFocusRef = useRef(false);\n  const requestRef = useRef<AbortController | null>(null);\n  const callbacksRef = useRef({ onDismiss });\n  const [closing, setClosing] = useState<ToastDismissReason | null>(null);\n  const [busy, setBusy] = useState<string | null>(null);\n  const [error, setError] = useState(\"\");\n  const { contextSafe } = useGSAP({ scope: cardRef });\n  const duration = bounded(item.duration ?? 0, 0, 0, 3600000);\n  const tone = item.tone ?? \"neutral\";\n  const hasFooter = Boolean(\n    item.footer ||\n    (item.countdownLabel && duration > 0) ||\n    (item.action && item.actionPlacement === \"footer\"),\n  );\n\n  useGSAP(\n    () => {\n      callbacksRef.current = { onDismiss };\n    },\n    { dependencies: [onDismiss] },\n  );\n\n  const pause = (reason: string, active: boolean) => {\n    if (active) pausesRef.current.add(reason);\n    else pausesRef.current.delete(reason);\n    timerRef.current?.paused(pausesRef.current.size > 0 || closingRef.current);\n  };\n\n  const dismiss = (reason: ToastDismissReason) => {\n    if (closingRef.current) return;\n    restoreFocusRef.current ||= Boolean(\n      cardRef.current?.contains(document.activeElement),\n    );\n    closingRef.current = true;\n    timerRef.current?.pause();\n    requestRef.current?.abort();\n    setClosing(reason);\n  };\n\n  useGSAP(\n    () => {\n      if (item.dismiss) dismiss(item.dismiss);\n    },\n    { dependencies: [item.dismiss], scope: cardRef },\n  );\n\n  useGSAP(\n    () => {\n      const reduced = window.matchMedia(\n        \"(prefers-reduced-motion: reduce)\",\n      ).matches;\n      gsap.fromTo(\n        cardRef.current,\n        { autoAlpha: 0, y: reduced ? 0 : 18, scale: reduced ? 1 : 0.97 },\n        {\n          autoAlpha: 1,\n          y: 0,\n          scale: 1,\n          duration: reduced ? 0 : motion.duration,\n          ease: motion.ease,\n          overwrite: \"auto\",\n        },\n      );\n      return () => {\n        requestRef.current?.abort();\n      };\n    },\n    { scope: cardRef },\n  );\n\n  useGSAP(\n    () => {\n      if (!closing) return;\n      gsap.to(cardRef.current, {\n        autoAlpha: 0,\n        y: motion.reduced ? 0 : -8,\n        scale: motion.reduced ? 1 : 0.97,\n        duration: motion.reduced ? 0 : 0.18,\n        ease: \"power2.in\",\n        overwrite: true,\n        onComplete: () =>\n          callbacksRef.current.onDismiss(\n            item.id,\n            closing,\n            restoreFocusRef.current,\n          ),\n      });\n    },\n    { dependencies: [closing, motion], scope: cardRef, revertOnUpdate: true },\n  );\n\n  useGSAP(\n    () => {\n      if (!busy || motion.reduced) return;\n      gsap.to(\"[data-toast-spinner]\", {\n        rotation: 360,\n        duration: 0.8,\n        ease: \"none\",\n        repeat: -1,\n      });\n    },\n    {\n      dependencies: [busy, motion.reduced],\n      scope: cardRef,\n      revertOnUpdate: true,\n    },\n  );\n\n  useGSAP(\n    () => {\n      if (!duration) return;\n      const clock = { remaining: duration / 1000 };\n      let lastSecond = Math.ceil(clock.remaining);\n      const timer = gsap.timeline({\n        paused: true,\n        onComplete: () => dismiss(\"timeout\"),\n      });\n      timer.to(\n        clock,\n        {\n          remaining: 0,\n          duration: duration / 1000,\n          ease: \"none\",\n          onUpdate: () => {\n            const second = Math.ceil(clock.remaining);\n            if (second !== lastSecond && secondsRef.current) {\n              secondsRef.current.textContent = `${second} ${second === 1 ? \"second\" : \"seconds\"}`;\n              lastSecond = second;\n            }\n          },\n        },\n        0,\n      );\n      timer.fromTo(\n        progressRef.current,\n        { scaleX: 1 },\n        { scaleX: 0, duration: duration / 1000, ease: \"none\" },\n        0,\n      );\n      timerRef.current = timer;\n      const visibility = () => pause(\"hidden\", document.hidden);\n      visibility();\n      document.addEventListener(\"visibilitychange\", visibility);\n      return () => {\n        document.removeEventListener(\"visibilitychange\", visibility);\n        timerRef.current = null;\n      };\n    },\n    { dependencies: [duration], scope: cardRef, revertOnUpdate: true },\n  );\n\n  const runAction = async (action: ToastAction) => {\n    if (requestRef.current || closingRef.current) return;\n    restoreFocusRef.current = Boolean(\n      cardRef.current?.contains(document.activeElement),\n    );\n    const request = new AbortController();\n    requestRef.current = request;\n    pause(\"action\", true);\n    setBusy(action.label);\n    setError(\"\");\n    try {\n      await action.onClick(request.signal);\n      if (request.signal.aborted) return;\n      if (action.dismissOnSuccess !== false) dismiss(\"action\");\n      pause(\"error\", false);\n    } catch {\n      if (!request.signal.aborted) {\n        setError(\n          action.errorMessage ??\n            \"Couldn\u2019t complete this action. Please try again.\",\n        );\n        pause(\"error\", true);\n      }\n    } finally {\n      if (!request.signal.aborted) {\n        requestRef.current = null;\n        setBusy(null);\n        pause(\"action\", false);\n      }\n    }\n  };\n\n  const highlightFooter = contextSafe(\n    (button: HTMLButtonElement, active: boolean) => {\n      const highlight = button.querySelector(\"[data-toast-footer-highlight]\");\n      if (!highlight) return;\n      gsap.to(highlight, {\n        opacity: active ? 1 : 0,\n        duration: motion.reduced ? 0 : 0.18,\n        ease: \"power2.out\",\n        overwrite: \"auto\",\n      });\n    },\n  );\n\n  const actionButton = (action: ToastAction, footer = false) => (\n    <button\n      type=\"button\"\n      disabled={busy !== null || closing !== null}\n      onClick={() => void runAction(action)}\n      onPointerEnter={\n        footer\n          ? (event) => highlightFooter(event.currentTarget, true)\n          : undefined\n      }\n      onPointerLeave={\n        footer\n          ? (event) => highlightFooter(event.currentTarget, false)\n          : undefined\n      }\n      onFocus={\n        footer\n          ? (event) => highlightFooter(event.currentTarget, true)\n          : undefined\n      }\n      onBlur={\n        footer\n          ? (event) => highlightFooter(event.currentTarget, false)\n          : undefined\n      }\n      className={cn(\n        buttonClass,\n        footer &&\n          \"relative isolate min-h-11 w-full overflow-hidden px-4 hover:bg-transparent dark:hover:bg-transparent\",\n        footer && !item.footer && \"rounded-none rounded-b-2xl\",\n        !footer && \"-ml-1.5\",\n      )}\n    >\n      {footer && (\n        <span\n          data-toast-footer-highlight\n          aria-hidden=\"true\"\n          className=\"pointer-events-none absolute inset-0 -z-10 bg-black/[0.06] opacity-0 dark:bg-white/[0.08]\"\n        />\n      )}\n      {busy === action.label && (\n        <LoaderCircle data-toast-spinner aria-hidden=\"true\" size={14} />\n      )}\n      {action.label}\n      {footer && item.footer && (\n        <ArrowRight aria-hidden=\"true\" size={14} className=\"ml-auto\" />\n      )}\n    </button>\n  );\n\n  return (\n    <div\n      ref={cardRef}\n      data-slot=\"toast\"\n      data-tone={tone}\n      data-state={closing ? \"closing\" : busy ? \"busy\" : \"open\"}\n      className={cn(\n        \"relative w-full rounded-2xl text-zinc-950 dark:text-zinc-100\",\n        hasFooter && \"bg-zinc-100 p-1 pb-0 dark:bg-[#191919]\",\n        tone === \"danger\" && \"bg-red-50 p-1 dark:bg-[#251414]\",\n        hasFooter && \"pb-0\",\n        tone === \"danger\"\n          ? \"[--toast-accent:#dc2626] dark:[--toast-accent:#f87171]\"\n          : tone === \"success\"\n            ? \"[--toast-accent:#15803d] dark:[--toast-accent:#4ade80]\"\n            : tone === \"info\"\n              ? \"[--toast-accent:#2563eb] dark:[--toast-accent:#60a5fa]\"\n              : \"[--toast-accent:#71717a] dark:[--toast-accent:#a1a1aa]\",\n      )}\n      onPointerEnter={(event) => {\n        if (event.pointerType !== \"touch\") pause(\"hover\", true);\n      }}\n      onPointerLeave={() => pause(\"hover\", false)}\n      onFocusCapture={() => pause(\"focus\", true)}\n      onBlurCapture={(event) => {\n        if (!event.currentTarget.contains(event.relatedTarget))\n          pause(\"focus\", false);\n      }}\n      onKeyDown={(event) => {\n        if (event.key === \"Escape\") {\n          event.preventDefault();\n          event.stopPropagation();\n          dismiss(\"dismiss\");\n        }\n      }}\n    >\n      <article\n        aria-labelledby={titleId}\n        aria-busy={busy !== null}\n        className={cn(\n          \"relative isolate overflow-hidden rounded-xl border border-zinc-200 bg-white shadow-[0_2px_4px_#0000000a] dark:border-white/10 dark:bg-[#0c0c0c] dark:shadow-none\",\n          tone === \"danger\" && \"border-red-200 dark:border-red-500/20\",\n        )}\n      >\n        <div\n          aria-hidden=\"true\"\n          className=\"pointer-events-none absolute inset-0 -z-10 opacity-[0.08] [mask-image:linear-gradient(to_right,transparent_45%,black)]\"\n          style={{\n            backgroundImage:\n              \"radial-gradient(var(--toast-accent) 0.8px, transparent 0.8px)\",\n            backgroundSize: \"7px 7px\",\n          }}\n        />\n        <div className=\"flex items-start gap-3 p-4 pr-11\">\n          {item.date ? (\n            <div className=\"flex size-12 shrink-0 flex-col items-center justify-center rounded-[10px] border border-zinc-100 bg-white text-center shadow-xs dark:border-white/10 dark:bg-black/30\">\n              <span className=\"text-[10px] font-medium uppercase text-zinc-500 dark:text-zinc-400\">\n                {item.date.month}\n              </span>\n              <span className=\"text-xl leading-6 font-medium tabular-nums\">\n                {item.date.day}\n              </span>\n            </div>\n          ) : (\n            item.icon !== null && (\n              <span className=\"flex size-8 shrink-0 items-center justify-center rounded-[9px] bg-zinc-100 text-(--toast-accent) dark:bg-white/5\">\n                {item.icon ??\n                  (tone === \"success\" ? (\n                    <Check size={17} aria-hidden=\"true\" />\n                  ) : (\n                    <Info size={17} aria-hidden=\"true\" />\n                  ))}\n              </span>\n            )\n          )}\n          <div className=\"min-w-0 flex-1\">\n            <h3\n              id={titleId}\n              className=\"text-[14px] leading-5 font-medium tracking-[-0.025em] wrap-anywhere\"\n            >\n              {item.title}\n            </h3>\n            {item.description && (\n              <p className=\"mt-1 text-[13px] leading-5 text-zinc-500 wrap-anywhere dark:text-zinc-400\">\n                {item.description}\n              </p>\n            )}\n            {(item.secondaryAction ||\n              (item.action && item.actionPlacement !== \"footer\")) && (\n              <div className=\"mt-2 flex flex-wrap items-center gap-x-3 gap-y-1\">\n                {item.secondaryAction ? (\n                  actionButton(item.secondaryAction)\n                ) : (\n                  <button\n                    type=\"button\"\n                    className={cn(\n                      buttonClass,\n                      \"-ml-1.5 text-zinc-500 dark:text-zinc-400\",\n                    )}\n                    onClick={() => dismiss(\"dismiss\")}\n                    disabled={busy !== null || closing !== null}\n                  >\n                    Dismiss\n                  </button>\n                )}\n                {item.action &&\n                  item.actionPlacement !== \"footer\" &&\n                  actionButton(item.action)}\n              </div>\n            )}\n            {error && (\n              <p\n                role=\"alert\"\n                className=\"mt-2 text-xs leading-5 text-red-600 dark:text-red-400\"\n              >\n                {error}\n              </p>\n            )}\n          </div>\n        </div>\n        <button\n          type=\"button\"\n          aria-label={`Dismiss ${item.title}`}\n          disabled={busy !== null || closing !== null}\n          onClick={() => dismiss(\"dismiss\")}\n          className={cn(\n            buttonClass,\n            \"absolute top-2 right-2 size-8 p-0 text-zinc-500 dark:text-zinc-400\",\n          )}\n        >\n          <X size={16} aria-hidden=\"true\" />\n        </button>\n        {item.badges && item.badges.length > 0 && (\n          <div className=\"flex flex-wrap gap-1.5 border-t border-zinc-100 bg-zinc-50/80 px-4 py-2.5 dark:border-white/5 dark:bg-white/[0.025]\">\n            {item.badges.map((badge, index) => (\n              <span\n                key={`${badge}-${index}`}\n                className={cn(\n                  \"rounded-md bg-zinc-200/70 px-2 py-1 text-[10px] leading-3 font-medium dark:bg-white/10\",\n                  index === 0 && \"bg-(--toast-accent)/10 text-(--toast-accent)\",\n                )}\n              >\n                {badge}\n              </span>\n            ))}\n          </div>\n        )}\n        {duration > 0 && (\n          <div\n            aria-hidden=\"true\"\n            className=\"absolute inset-x-0 bottom-0 h-0.5 bg-(--toast-accent)/10\"\n          >\n            <div\n              ref={progressRef}\n              className=\"h-full origin-left bg-(--toast-accent)/50\"\n            />\n          </div>\n        )}\n      </article>\n      {hasFooter && (\n        <div\n          className={cn(\n            \"flex min-h-8 flex-wrap items-center justify-center gap-x-1 px-2 text-center text-[11px] leading-5 text-zinc-500 dark:text-zinc-400\",\n            tone === \"danger\" && \"text-red-600 dark:text-red-400\",\n            item.action &&\n              item.actionPlacement === \"footer\" &&\n              !item.footer &&\n              \"-mx-1 overflow-hidden rounded-b-2xl px-0\",\n          )}\n        >\n          {item.countdownLabel && duration > 0 ? (\n            <>\n              <span>{item.countdownLabel}</span>\n              <span\n                ref={secondsRef}\n                className=\"font-medium text-(--toast-accent) tabular-nums\"\n              >\n                {Math.ceil(duration / 1000)}{\" \"}\n                {Math.ceil(duration / 1000) === 1 ? \"second\" : \"seconds\"}\n              </span>\n            </>\n          ) : (\n            item.footer && (\n              <span className={cn(item.action && \"mr-auto pl-2 text-left\")}>\n                {item.footer}\n              </span>\n            )\n          )}\n          {item.action && item.actionPlacement === \"footer\" && (\n            <div className={cn(\"flex\", item.footer ? \"ml-auto\" : \"w-full\")}>\n              {actionButton(item.action, true)}\n            </div>\n          )}\n        </div>\n      )}\n    </div>\n  );\n}\n\nfunction measureToastPositions(root: HTMLDivElement | null) {\n  const rows = Array.from(\n    root?.querySelectorAll<HTMLLIElement>(\"[data-toast-id]\") ?? [],\n  );\n  return rows.map((row) => {\n    const rect = row.getBoundingClientRect();\n    const currentX = Number(gsap.getProperty(row, \"x\")) || 0;\n    const currentY = Number(gsap.getProperty(row, \"y\")) || 0;\n    return {\n      row,\n      id: row.dataset.toastId ?? \"\",\n      x: rect.left - currentX,\n      y: rect.top - currentY,\n      currentX,\n      currentY,\n    };\n  });\n}\n\nexport function Toaster({\n  items,\n  onDismiss,\n  stiffness = 240,\n  damping = 28,\n  className,\n  ref,\n}: ToasterProps) {\n  const rootRef = useRef<HTMLDivElement>(null);\n  const positionsRef = useRef(new Map<string, { x: number; y: number }>());\n  const restoreFocusRef = useRef(false);\n  const motion = useToastMotion(stiffness, damping);\n  const itemKey = JSON.stringify(items.map((item) => item.id));\n\n  useGSAP(\n    () => {\n      const positions = measureToastPositions(rootRef.current);\n      for (const { row, id, x, y, currentX, currentY } of positions) {\n        const previous = positionsRef.current.get(id);\n        if (previous) {\n          gsap.fromTo(\n            row,\n            { x: previous.x - x + currentX, y: previous.y - y + currentY },\n            {\n              x: 0,\n              y: 0,\n              duration: motion.duration,\n              ease: motion.ease,\n              overwrite: true,\n            },\n          );\n        }\n      }\n      positionsRef.current = new Map(\n        positions.map(({ id, x, y }) => [id, { x, y }]),\n      );\n      if (restoreFocusRef.current) {\n        (\n          positions[0]?.row.querySelector<HTMLButtonElement>(\n            \"button:not(:disabled)\",\n          ) ?? rootRef.current\n        )?.focus({ preventScroll: true });\n        restoreFocusRef.current = false;\n      }\n    },\n    { dependencies: [itemKey, motion, className], scope: rootRef },\n  );\n\n  return (\n    <div\n      ref={(node) => {\n        rootRef.current = node;\n        if (typeof ref === \"function\") ref(node);\n        else if (ref) ref.current = node;\n      }}\n      role=\"region\"\n      aria-label=\"Notifications\"\n      tabIndex={-1}\n      className=\"w-full outline-none focus-visible:ring-2 focus-visible:ring-zinc-400\"\n    >\n      <ol\n        className={cn(\n          \"relative mx-auto flex w-full max-w-[440px] flex-col gap-4\",\n          className,\n        )}\n      >\n        {items.map((item) => (\n          <li\n            key={item.id}\n            data-toast-id={item.id}\n            className=\"relative min-w-0\"\n          >\n            <ToastCard\n              item={item}\n              motion={motion}\n              onDismiss={(id, reason, restoreFocus) => {\n                positionsRef.current = new Map(\n                  measureToastPositions(rootRef.current).map(\n                    ({ id: toastId, x, y }) => [toastId, { x, y }],\n                  ),\n                );\n                const row = Array.from(\n                  rootRef.current?.querySelectorAll<HTMLLIElement>(\n                    \"[data-toast-id]\",\n                  ) ?? [],\n                ).find((element) => element.dataset.toastId === id);\n                const activeElement = document.activeElement;\n                const containsFocus = Boolean(row?.contains(activeElement));\n                restoreFocusRef.current ||=\n                  containsFocus ||\n                  (restoreFocus && activeElement === document.body);\n                onDismiss(id, reason);\n              }}\n            />\n          </li>\n        ))}\n      </ol>\n      <div className=\"sr-only\" aria-live=\"polite\" aria-atomic=\"true\">\n        {items.map((item) => item.title).join(\". \")}\n      </div>\n    </div>\n  );\n}\n\nexport default Toaster;\n",
      "content": "\"use client\";\n\nimport {\n  useId,\n  useMemo,\n  useRef,\n  useState,\n  type ReactNode,\n  type Ref,\n} from \"react\";\nimport { useGSAP } from \"@gsap/react\";\nimport gsap from \"gsap\";\nimport { calcGeneratorDuration, spring } from \"motion\";\nimport { ArrowRight, Check, Info, LoaderCircle, X } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";\nimport { useMediaQuery } from \"@/lib/use-media-query\";\n\ngsap.registerPlugin(useGSAP);\n\nexport type ToastDismissReason = \"dismiss\" | \"timeout\" | \"action\";\n\nexport interface ToastAction {\n  label: string;\n  onClick: (signal: AbortSignal) => void | Promise<void>;\n  dismissOnSuccess?: boolean;\n  errorMessage?: string;\n}\n\nexport interface ToastItem {\n  id: string;\n  title: string;\n  description?: string;\n  tone?: \"neutral\" | \"info\" | \"danger\" | \"success\";\n  icon?: ReactNode;\n  date?: { month: string; day: string };\n  badges?: readonly string[];\n  duration?: number;\n  countdownLabel?: string;\n  footer?: string;\n  action?: ToastAction;\n  secondaryAction?: ToastAction;\n  actionPlacement?: \"inline\" | \"footer\";\n  dismiss?: ToastDismissReason;\n}\n\nexport interface ToasterProps {\n  items: readonly ToastItem[];\n  onDismiss: (id: string, reason: ToastDismissReason) => void;\n  stiffness?: number;\n  damping?: number;\n  className?: string;\n  ref?: Ref<HTMLDivElement>;\n}\n\nconst bounded = (value: number, fallback: number, min: number, max: number) =>\n  Number.isFinite(value) ? Math.min(max, Math.max(min, value)) : fallback;\n\nfunction useToastMotion(stiffness: number, damping: number) {\n  const reduced = useMediaQuery(\"(prefers-reduced-motion: reduce)\");\n  return useMemo(() => {\n    const response = spring({\n      keyframes: [0, 1],\n      stiffness: bounded(stiffness, 240, 80, 500),\n      damping: bounded(damping, 28, 16, 50),\n      restSpeed: 0.001,\n      restDelta: 0.001,\n    });\n    const duration = calcGeneratorDuration(response) / 1000;\n    return {\n      reduced,\n      duration: reduced ? 0 : duration,\n      ease: (progress: number) =>\n        progress === 1 ? 1 : response.next(progress * duration * 1000).value,\n    };\n  }, [stiffness, damping, reduced]);\n}\n\nconst buttonClass =\n  \"inline-flex min-h-8 cursor-pointer items-center justify-center gap-2 rounded-md px-1.5 text-[13px] font-medium outline-none hover:bg-black/5 focus-visible:ring-2 focus-visible:ring-current disabled:cursor-wait disabled:opacity-50 dark:hover:bg-white/10\";\n\nfunction ToastCard({\n  item,\n  onDismiss,\n  motion,\n}: {\n  item: ToastItem;\n  onDismiss: (\n    id: string,\n    reason: ToastDismissReason,\n    restoreFocus: boolean,\n  ) => void;\n  motion: ReturnType<typeof useToastMotion>;\n}) {\n  const titleId = useId();\n  const cardRef = useRef<HTMLDivElement>(null);\n  const secondsRef = useRef<HTMLSpanElement>(null);\n  const progressRef = useRef<HTMLDivElement>(null);\n  const timerRef = useRef<gsap.core.Timeline | null>(null);\n  const pausesRef = useRef(new Set<string>());\n  const closingRef = useRef(false);\n  const restoreFocusRef = useRef(false);\n  const requestRef = useRef<AbortController | null>(null);\n  const callbacksRef = useRef({ onDismiss });\n  const [closing, setClosing] = useState<ToastDismissReason | null>(null);\n  const [busy, setBusy] = useState<string | null>(null);\n  const [error, setError] = useState(\"\");\n  const { contextSafe } = useGSAP({ scope: cardRef });\n  const duration = bounded(item.duration ?? 0, 0, 0, 3600000);\n  const tone = item.tone ?? \"neutral\";\n  const hasFooter = Boolean(\n    item.footer ||\n    (item.countdownLabel && duration > 0) ||\n    (item.action && item.actionPlacement === \"footer\"),\n  );\n\n  useGSAP(\n    () => {\n      callbacksRef.current = { onDismiss };\n    },\n    { dependencies: [onDismiss] },\n  );\n\n  const pause = (reason: string, active: boolean) => {\n    if (active) pausesRef.current.add(reason);\n    else pausesRef.current.delete(reason);\n    timerRef.current?.paused(pausesRef.current.size > 0 || closingRef.current);\n  };\n\n  const dismiss = (reason: ToastDismissReason) => {\n    if (closingRef.current) return;\n    restoreFocusRef.current ||= Boolean(\n      cardRef.current?.contains(document.activeElement),\n    );\n    closingRef.current = true;\n    timerRef.current?.pause();\n    requestRef.current?.abort();\n    setClosing(reason);\n  };\n\n  useGSAP(\n    () => {\n      if (item.dismiss) dismiss(item.dismiss);\n    },\n    { dependencies: [item.dismiss], scope: cardRef },\n  );\n\n  useGSAP(\n    () => {\n      const reduced = window.matchMedia(\n        \"(prefers-reduced-motion: reduce)\",\n      ).matches;\n      gsap.fromTo(\n        cardRef.current,\n        { autoAlpha: 0, y: reduced ? 0 : 18, scale: reduced ? 1 : 0.97 },\n        {\n          autoAlpha: 1,\n          y: 0,\n          scale: 1,\n          duration: reduced ? 0 : motion.duration,\n          ease: motion.ease,\n          overwrite: \"auto\",\n        },\n      );\n      return () => {\n        requestRef.current?.abort();\n      };\n    },\n    { scope: cardRef },\n  );\n\n  useGSAP(\n    () => {\n      if (!closing) return;\n      gsap.to(cardRef.current, {\n        autoAlpha: 0,\n        y: motion.reduced ? 0 : -8,\n        scale: motion.reduced ? 1 : 0.97,\n        duration: motion.reduced ? 0 : 0.18,\n        ease: \"power2.in\",\n        overwrite: true,\n        onComplete: () =>\n          callbacksRef.current.onDismiss(\n            item.id,\n            closing,\n            restoreFocusRef.current,\n          ),\n      });\n    },\n    { dependencies: [closing, motion], scope: cardRef, revertOnUpdate: true },\n  );\n\n  useGSAP(\n    () => {\n      if (!busy || motion.reduced) return;\n      gsap.to(\"[data-toast-spinner]\", {\n        rotation: 360,\n        duration: 0.8,\n        ease: \"none\",\n        repeat: -1,\n      });\n    },\n    {\n      dependencies: [busy, motion.reduced],\n      scope: cardRef,\n      revertOnUpdate: true,\n    },\n  );\n\n  useGSAP(\n    () => {\n      if (!duration) return;\n      const clock = { remaining: duration / 1000 };\n      let lastSecond = Math.ceil(clock.remaining);\n      const timer = gsap.timeline({\n        paused: true,\n        onComplete: () => dismiss(\"timeout\"),\n      });\n      timer.to(\n        clock,\n        {\n          remaining: 0,\n          duration: duration / 1000,\n          ease: \"none\",\n          onUpdate: () => {\n            const second = Math.ceil(clock.remaining);\n            if (second !== lastSecond && secondsRef.current) {\n              secondsRef.current.textContent = `${second} ${second === 1 ? \"second\" : \"seconds\"}`;\n              lastSecond = second;\n            }\n          },\n        },\n        0,\n      );\n      timer.fromTo(\n        progressRef.current,\n        { scaleX: 1 },\n        { scaleX: 0, duration: duration / 1000, ease: \"none\" },\n        0,\n      );\n      timerRef.current = timer;\n      const visibility = () => pause(\"hidden\", document.hidden);\n      visibility();\n      document.addEventListener(\"visibilitychange\", visibility);\n      return () => {\n        document.removeEventListener(\"visibilitychange\", visibility);\n        timerRef.current = null;\n      };\n    },\n    { dependencies: [duration], scope: cardRef, revertOnUpdate: true },\n  );\n\n  const runAction = async (action: ToastAction) => {\n    if (requestRef.current || closingRef.current) return;\n    restoreFocusRef.current = Boolean(\n      cardRef.current?.contains(document.activeElement),\n    );\n    const request = new AbortController();\n    requestRef.current = request;\n    pause(\"action\", true);\n    setBusy(action.label);\n    setError(\"\");\n    try {\n      await action.onClick(request.signal);\n      if (request.signal.aborted) return;\n      if (action.dismissOnSuccess !== false) dismiss(\"action\");\n      pause(\"error\", false);\n    } catch {\n      if (!request.signal.aborted) {\n        setError(\n          action.errorMessage ??\n            \"Couldn\u2019t complete this action. Please try again.\",\n        );\n        pause(\"error\", true);\n      }\n    } finally {\n      if (!request.signal.aborted) {\n        requestRef.current = null;\n        setBusy(null);\n        pause(\"action\", false);\n      }\n    }\n  };\n\n  const highlightFooter = contextSafe(\n    (button: HTMLButtonElement, active: boolean) => {\n      const highlight = button.querySelector(\"[data-toast-footer-highlight]\");\n      if (!highlight) return;\n      gsap.to(highlight, {\n        opacity: active ? 1 : 0,\n        duration: motion.reduced ? 0 : 0.18,\n        ease: \"power2.out\",\n        overwrite: \"auto\",\n      });\n    },\n  );\n\n  const actionButton = (action: ToastAction, footer = false) => (\n    <button\n      type=\"button\"\n      disabled={busy !== null || closing !== null}\n      onClick={() => void runAction(action)}\n      onPointerEnter={\n        footer\n          ? (event) => highlightFooter(event.currentTarget, true)\n          : undefined\n      }\n      onPointerLeave={\n        footer\n          ? (event) => highlightFooter(event.currentTarget, false)\n          : undefined\n      }\n      onFocus={\n        footer\n          ? (event) => highlightFooter(event.currentTarget, true)\n          : undefined\n      }\n      onBlur={\n        footer\n          ? (event) => highlightFooter(event.currentTarget, false)\n          : undefined\n      }\n      className={cn(\n        buttonClass,\n        footer &&\n          \"relative isolate min-h-11 w-full overflow-hidden px-4 hover:bg-transparent dark:hover:bg-transparent\",\n        footer && !item.footer && \"rounded-none rounded-b-2xl\",\n        !footer && \"-ml-1.5\",\n      )}\n    >\n      {footer && (\n        <span\n          data-toast-footer-highlight\n          aria-hidden=\"true\"\n          className=\"pointer-events-none absolute inset-0 -z-10 bg-black/[0.06] opacity-0 dark:bg-white/[0.08]\"\n        />\n      )}\n      {busy === action.label && (\n        <LoaderCircle data-toast-spinner aria-hidden=\"true\" size={14} />\n      )}\n      {action.label}\n      {footer && item.footer && (\n        <ArrowRight aria-hidden=\"true\" size={14} className=\"ml-auto\" />\n      )}\n    </button>\n  );\n\n  return (\n    <div\n      ref={cardRef}\n      data-slot=\"toast\"\n      data-tone={tone}\n      data-state={closing ? \"closing\" : busy ? \"busy\" : \"open\"}\n      className={cn(\n        \"relative w-full rounded-2xl text-zinc-950 dark:text-zinc-100\",\n        hasFooter && \"bg-zinc-100 p-1 pb-0 dark:bg-[#191919]\",\n        tone === \"danger\" && \"bg-red-50 p-1 dark:bg-[#251414]\",\n        hasFooter && \"pb-0\",\n        tone === \"danger\"\n          ? \"[--toast-accent:#dc2626] dark:[--toast-accent:#f87171]\"\n          : tone === \"success\"\n            ? \"[--toast-accent:#15803d] dark:[--toast-accent:#4ade80]\"\n            : tone === \"info\"\n              ? \"[--toast-accent:#2563eb] dark:[--toast-accent:#60a5fa]\"\n              : \"[--toast-accent:#71717a] dark:[--toast-accent:#a1a1aa]\",\n      )}\n      onPointerEnter={(event) => {\n        if (event.pointerType !== \"touch\") pause(\"hover\", true);\n      }}\n      onPointerLeave={() => pause(\"hover\", false)}\n      onFocusCapture={() => pause(\"focus\", true)}\n      onBlurCapture={(event) => {\n        if (!event.currentTarget.contains(event.relatedTarget))\n          pause(\"focus\", false);\n      }}\n      onKeyDown={(event) => {\n        if (event.key === \"Escape\") {\n          event.preventDefault();\n          event.stopPropagation();\n          dismiss(\"dismiss\");\n        }\n      }}\n    >\n      <article\n        aria-labelledby={titleId}\n        aria-busy={busy !== null}\n        className={cn(\n          \"relative isolate overflow-hidden rounded-xl border border-zinc-200 bg-white shadow-[0_2px_4px_#0000000a] dark:border-white/10 dark:bg-[#0c0c0c] dark:shadow-none\",\n          tone === \"danger\" && \"border-red-200 dark:border-red-500/20\",\n        )}\n      >\n        <div\n          aria-hidden=\"true\"\n          className=\"pointer-events-none absolute inset-0 -z-10 opacity-[0.08] [mask-image:linear-gradient(to_right,transparent_45%,black)]\"\n          style={{\n            backgroundImage:\n              \"radial-gradient(var(--toast-accent) 0.8px, transparent 0.8px)\",\n            backgroundSize: \"7px 7px\",\n          }}\n        />\n        <div className=\"flex items-start gap-3 p-4 pr-11\">\n          {item.date ? (\n            <div className=\"flex size-12 shrink-0 flex-col items-center justify-center rounded-[10px] border border-zinc-100 bg-white text-center shadow-xs dark:border-white/10 dark:bg-black/30\">\n              <span className=\"text-[10px] font-medium uppercase text-zinc-500 dark:text-zinc-400\">\n                {item.date.month}\n              </span>\n              <span className=\"text-xl leading-6 font-medium tabular-nums\">\n                {item.date.day}\n              </span>\n            </div>\n          ) : (\n            item.icon !== null && (\n              <span className=\"flex size-8 shrink-0 items-center justify-center rounded-[9px] bg-zinc-100 text-(--toast-accent) dark:bg-white/5\">\n                {item.icon ??\n                  (tone === \"success\" ? (\n                    <Check size={17} aria-hidden=\"true\" />\n                  ) : (\n                    <Info size={17} aria-hidden=\"true\" />\n                  ))}\n              </span>\n            )\n          )}\n          <div className=\"min-w-0 flex-1\">\n            <h3\n              id={titleId}\n              className=\"text-[14px] leading-5 font-medium tracking-[-0.025em] wrap-anywhere\"\n            >\n              {item.title}\n            </h3>\n            {item.description && (\n              <p className=\"mt-1 text-[13px] leading-5 text-zinc-500 wrap-anywhere dark:text-zinc-400\">\n                {item.description}\n              </p>\n            )}\n            {(item.secondaryAction ||\n              (item.action && item.actionPlacement !== \"footer\")) && (\n              <div className=\"mt-2 flex flex-wrap items-center gap-x-3 gap-y-1\">\n                {item.secondaryAction ? (\n                  actionButton(item.secondaryAction)\n                ) : (\n                  <button\n                    type=\"button\"\n                    className={cn(\n                      buttonClass,\n                      \"-ml-1.5 text-zinc-500 dark:text-zinc-400\",\n                    )}\n                    onClick={() => dismiss(\"dismiss\")}\n                    disabled={busy !== null || closing !== null}\n                  >\n                    Dismiss\n                  </button>\n                )}\n                {item.action &&\n                  item.actionPlacement !== \"footer\" &&\n                  actionButton(item.action)}\n              </div>\n            )}\n            {error && (\n              <p\n                role=\"alert\"\n                className=\"mt-2 text-xs leading-5 text-red-600 dark:text-red-400\"\n              >\n                {error}\n              </p>\n            )}\n          </div>\n        </div>\n        <button\n          type=\"button\"\n          aria-label={`Dismiss ${item.title}`}\n          disabled={busy !== null || closing !== null}\n          onClick={() => dismiss(\"dismiss\")}\n          className={cn(\n            buttonClass,\n            \"absolute top-2 right-2 size-8 p-0 text-zinc-500 dark:text-zinc-400\",\n          )}\n        >\n          <X size={16} aria-hidden=\"true\" />\n        </button>\n        {item.badges && item.badges.length > 0 && (\n          <div className=\"flex flex-wrap gap-1.5 border-t border-zinc-100 bg-zinc-50/80 px-4 py-2.5 dark:border-white/5 dark:bg-white/[0.025]\">\n            {item.badges.map((badge, index) => (\n              <span\n                key={`${badge}-${index}`}\n                className={cn(\n                  \"rounded-md bg-zinc-200/70 px-2 py-1 text-[10px] leading-3 font-medium dark:bg-white/10\",\n                  index === 0 && \"bg-(--toast-accent)/10 text-(--toast-accent)\",\n                )}\n              >\n                {badge}\n              </span>\n            ))}\n          </div>\n        )}\n        {duration > 0 && (\n          <div\n            aria-hidden=\"true\"\n            className=\"absolute inset-x-0 bottom-0 h-0.5 bg-(--toast-accent)/10\"\n          >\n            <div\n              ref={progressRef}\n              className=\"h-full origin-left bg-(--toast-accent)/50\"\n            />\n          </div>\n        )}\n      </article>\n      {hasFooter && (\n        <div\n          className={cn(\n            \"flex min-h-8 flex-wrap items-center justify-center gap-x-1 px-2 text-center text-[11px] leading-5 text-zinc-500 dark:text-zinc-400\",\n            tone === \"danger\" && \"text-red-600 dark:text-red-400\",\n            item.action &&\n              item.actionPlacement === \"footer\" &&\n              !item.footer &&\n              \"-mx-1 overflow-hidden rounded-b-2xl px-0\",\n          )}\n        >\n          {item.countdownLabel && duration > 0 ? (\n            <>\n              <span>{item.countdownLabel}</span>\n              <span\n                ref={secondsRef}\n                className=\"font-medium text-(--toast-accent) tabular-nums\"\n              >\n                {Math.ceil(duration / 1000)}{\" \"}\n                {Math.ceil(duration / 1000) === 1 ? \"second\" : \"seconds\"}\n              </span>\n            </>\n          ) : (\n            item.footer && (\n              <span className={cn(item.action && \"mr-auto pl-2 text-left\")}>\n                {item.footer}\n              </span>\n            )\n          )}\n          {item.action && item.actionPlacement === \"footer\" && (\n            <div className={cn(\"flex\", item.footer ? \"ml-auto\" : \"w-full\")}>\n              {actionButton(item.action, true)}\n            </div>\n          )}\n        </div>\n      )}\n    </div>\n  );\n}\n\nfunction measureToastPositions(root: HTMLDivElement | null) {\n  const rows = Array.from(\n    root?.querySelectorAll<HTMLLIElement>(\"[data-toast-id]\") ?? [],\n  );\n  return rows.map((row) => {\n    const rect = row.getBoundingClientRect();\n    const currentX = Number(gsap.getProperty(row, \"x\")) || 0;\n    const currentY = Number(gsap.getProperty(row, \"y\")) || 0;\n    return {\n      row,\n      id: row.dataset.toastId ?? \"\",\n      x: rect.left - currentX,\n      y: rect.top - currentY,\n      currentX,\n      currentY,\n    };\n  });\n}\n\nexport function Toaster({\n  items,\n  onDismiss,\n  stiffness = 240,\n  damping = 28,\n  className,\n  ref,\n}: ToasterProps) {\n  const rootRef = useRef<HTMLDivElement>(null);\n  const positionsRef = useRef(new Map<string, { x: number; y: number }>());\n  const restoreFocusRef = useRef(false);\n  const motion = useToastMotion(stiffness, damping);\n  const itemKey = JSON.stringify(items.map((item) => item.id));\n\n  useGSAP(\n    () => {\n      const positions = measureToastPositions(rootRef.current);\n      for (const { row, id, x, y, currentX, currentY } of positions) {\n        const previous = positionsRef.current.get(id);\n        if (previous) {\n          gsap.fromTo(\n            row,\n            { x: previous.x - x + currentX, y: previous.y - y + currentY },\n            {\n              x: 0,\n              y: 0,\n              duration: motion.duration,\n              ease: motion.ease,\n              overwrite: true,\n            },\n          );\n        }\n      }\n      positionsRef.current = new Map(\n        positions.map(({ id, x, y }) => [id, { x, y }]),\n      );\n      if (restoreFocusRef.current) {\n        (\n          positions[0]?.row.querySelector<HTMLButtonElement>(\n            \"button:not(:disabled)\",\n          ) ?? rootRef.current\n        )?.focus({ preventScroll: true });\n        restoreFocusRef.current = false;\n      }\n    },\n    { dependencies: [itemKey, motion, className], scope: rootRef },\n  );\n\n  return (\n    <div\n      ref={(node) => {\n        rootRef.current = node;\n        if (typeof ref === \"function\") ref(node);\n        else if (ref) ref.current = node;\n      }}\n      role=\"region\"\n      aria-label=\"Notifications\"\n      tabIndex={-1}\n      className=\"w-full outline-none focus-visible:ring-2 focus-visible:ring-zinc-400\"\n    >\n      <ol\n        className={cn(\n          \"relative mx-auto flex w-full max-w-[440px] flex-col gap-4\",\n          className,\n        )}\n      >\n        {items.map((item) => (\n          <li\n            key={item.id}\n            data-toast-id={item.id}\n            className=\"relative min-w-0\"\n          >\n            <ToastCard\n              item={item}\n              motion={motion}\n              onDismiss={(id, reason, restoreFocus) => {\n                positionsRef.current = new Map(\n                  measureToastPositions(rootRef.current).map(\n                    ({ id: toastId, x, y }) => [toastId, { x, y }],\n                  ),\n                );\n                const row = Array.from(\n                  rootRef.current?.querySelectorAll<HTMLLIElement>(\n                    \"[data-toast-id]\",\n                  ) ?? [],\n                ).find((element) => element.dataset.toastId === id);\n                const activeElement = document.activeElement;\n                const containsFocus = Boolean(row?.contains(activeElement));\n                restoreFocusRef.current ||=\n                  containsFocus ||\n                  (restoreFocus && activeElement === document.body);\n                onDismiss(id, reason);\n              }}\n            />\n          </li>\n        ))}\n      </ol>\n      <div className=\"sr-only\" aria-live=\"polite\" aria-atomic=\"true\">\n        {items.map((item) => item.title).join(\". \")}\n      </div>\n    </div>\n  );\n}\n\nexport default Toaster;\n",
      "target": "@ui/toaster.tsx"
    },
    {
      "name": "use-media-query.ts",
      "path": "lib/use-media-query.ts",
      "type": "registry:lib",
      "code": "\"use client\";\n\nimport { useCallback, useSyncExternalStore } from \"react\";\n\nexport function useMediaQuery(query: string) {\n  const subscribe = useCallback(\n    (onChange: () => void) => {\n      const media = window.matchMedia(query);\n      media.addEventListener(\"change\", onChange);\n      return () => media.removeEventListener(\"change\", onChange);\n    },\n    [query],\n  );\n  const getSnapshot = useCallback(() => window.matchMedia(query).matches, [query]);\n  return useSyncExternalStore(subscribe, getSnapshot, () => false);\n}\n",
      "content": "\"use client\";\n\nimport { useCallback, useSyncExternalStore } from \"react\";\n\nexport function useMediaQuery(query: string) {\n  const subscribe = useCallback(\n    (onChange: () => void) => {\n      const media = window.matchMedia(query);\n      media.addEventListener(\"change\", onChange);\n      return () => media.removeEventListener(\"change\", onChange);\n    },\n    [query],\n  );\n  const getSnapshot = useCallback(() => window.matchMedia(query).matches, [query]);\n  return useSyncExternalStore(subscribe, getSnapshot, () => false);\n}\n"
    }
  ]
}
