{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "toast",
  "title": "Toast",
  "description": "A brief message about something that just happened, which waits while you read it and leaves without being asked.",
  "dependencies": [
    "motion",
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://miseui.junsu4621.workers.dev/r/motion.json"
  ],
  "files": [
    {
      "path": "registry/miseui/ui/toast.tsx",
      "content": "import type * as React from \"react\"\nimport { useCallback, useMemo, useState, useSyncExternalStore } from \"react\"\nimport { AnimatePresence } from \"motion/react\"\nimport { ToastItem, resolveSwipe, type ToastEffect, type ToastVariant } from \"@/registry/miseui/ui/toast-item\"\nimport {\n  DECK_GAP_PX,\n  DEFAULT_MAX,\n  FALLBACK_HEIGHT_PX,\n  deckPlacement,\n  regionClass,\n  type ToastPosition,\n} from \"@/registry/miseui/ui/toast-layout\"\nimport {\n  DEFAULT_TOASTER,\n  toast,\n  toastStore,\n  type ToastAction,\n  type ToastOptions,\n  type ToastPromiseMessages,\n  type ToastRecord,\n  type ToastType,\n} from \"@/registry/miseui/ui/toast-store\"\nimport { cn } from \"@/registry/miseui/lib/utils\"\n\ninterface ToasterProps extends Omit<React.ComponentProps<\"section\">, \"id\"> {\n  /** Only shows toasts sent with the same `toaster` option. */\n  id?: string\n  /** `stack` a deck that fans out on hover, `list` a plain column, `pill` one compact capsule, `bar` a full-width banner at the edge. */\n  variant?: ToastVariant\n  /** `spring` brings toasts in on a spring and lets them be swiped away with momentum. */\n  effect?: ToastEffect\n  /** `pill` and `bar` only use the top or bottom part. */\n  position?: ToastPosition\n  /** How many show at once; the rest wait their turn. */\n  max?: number\n  /** Position inside the nearest positioned ancestor, not the viewport. For previews. */\n  contained?: boolean\n}\n\nconst announcement = (record: ToastRecord) =>\n  record.description ? `${record.title}. ${record.description}` : record.title\n\nfunction useToasts(id: string) {\n  const all = useSyncExternalStore(toastStore.subscribe, toastStore.getSnapshot, toastStore.getServerSnapshot)\n  return useMemo(() => all.filter((record) => record.toaster === id), [all, id])\n}\n\n/** Hover or focus inside the region pauses every timer and fans out the deck. */\nfunction useRegionAttention(hasToasts: boolean) {\n  const [hovered, setHovered] = useState(false)\n  const [focused, setFocused] = useState(false)\n  // Removing the last toast under a still pointer fires no leave event; without this a later toast would stay paused.\n  if (!hasToasts && (hovered || focused)) {\n    setHovered(false)\n    setFocused(false)\n  }\n  const handlers = {\n    onPointerEnter: () => setHovered(true),\n    onPointerLeave: () => setHovered(false),\n    onFocus: () => setFocused(true),\n    onBlur: (event: React.FocusEvent<HTMLElement>) => {\n      if (!event.currentTarget.contains(event.relatedTarget)) setFocused(false)\n    },\n  }\n  return { expanded: hovered || focused, setFocused, handlers }\n}\n\nfunction useMeasuredHeights() {\n  const [measured, setMeasured] = useState<Readonly<Record<string, number>>>({})\n  const onMeasure = useCallback((id: string, height: number | undefined) => {\n    setMeasured((current) => {\n      if (!height) return Object.fromEntries(Object.entries(current).filter(([key]) => key !== id))\n      return current[id] === height ? current : { ...current, [id]: height }\n    })\n  }, [])\n  return { measured, onMeasure }\n}\n\n/** Live regions must exist before their text changes, or screen readers miss it; toast cards come and go. */\nfunction LiveRegions({ latest }: { latest: ToastRecord | undefined }) {\n  return (\n    <>\n      <div role=\"status\" aria-live=\"polite\" aria-atomic=\"true\" className=\"sr-only\">\n        {latest && latest.type !== \"error\" ? announcement(latest) : \"\"}\n      </div>\n      <div role=\"alert\" aria-live=\"assertive\" aria-atomic=\"true\" className=\"sr-only\">\n        {latest?.type === \"error\" ? announcement(latest) : \"\"}\n      </div>\n    </>\n  )\n}\n\ninterface ToastListProps {\n  visible: readonly ToastRecord[]\n  variant: ToastVariant\n  effect: ToastEffect | undefined\n  isTop: boolean\n  expanded: boolean\n  onDismiss: (id: string) => void\n}\n\nfunction ToastList({ visible, variant, effect, isTop, expanded, onDismiss }: ToastListProps) {\n  const { measured, onMeasure } = useMeasuredHeights()\n  const direction = isTop ? 1 : -1\n  const isDeck = variant === \"stack\"\n  const heights = visible.map((record) => measured[record.id] ?? FALLBACK_HEIGHT_PX)\n  const deckHeight = expanded ? heights.reduce((sum, height) => sum + height + DECK_GAP_PX, -DECK_GAP_PX) : (heights[0] ?? 0)\n  return (\n    <ol\n      className={cn(\n        \"relative\",\n        visible.length > 0 && \"pointer-events-auto\",\n        !isDeck && [\"flex\", variant !== \"bar\" && \"gap-2\", isTop ? \"flex-col\" : \"flex-col-reverse\"],\n        variant === \"pill\" && \"items-center\",\n      )}\n      style={isDeck ? { height: deckHeight } : undefined}\n    >\n      <AnimatePresence initial={false} mode={isDeck ? \"sync\" : \"popLayout\"}>\n        {visible.map((record, index) => (\n          <ToastItem\n            key={record.id}\n            record={record}\n            variant={variant}\n            effect={effect}\n            direction={direction}\n            paused={expanded}\n            placement={isDeck ? deckPlacement({ index, expanded, heights, direction }) : undefined}\n            onDismiss={onDismiss}\n            onExpire={toast.dismiss}\n            onMeasure={onMeasure}\n          />\n        ))}\n      </AnimatePresence>\n    </ol>\n  )\n}\n\nfunction Toaster({\n  id = DEFAULT_TOASTER,\n  variant = \"stack\",\n  effect,\n  position = \"bottom-right\",\n  max,\n  contained = false,\n  className,\n  \"aria-label\": ariaLabel = \"Notifications\",\n  ...props\n}: ToasterProps) {\n  const toasts = useToasts(id)\n  const { expanded, setFocused, handlers } = useRegionAttention(toasts.length > 0)\n  const onDismiss = useCallback(\n    (toastId: string) => {\n      // The focused button is about to leave the DOM, and browsers do not reliably fire blur for that.\n      setFocused(false)\n      toast.dismiss(toastId)\n    },\n    [setFocused],\n  )\n  return (\n    <section\n      aria-label={ariaLabel}\n      data-slot=\"toaster\"\n      data-variant={variant}\n      data-effect={effect}\n      data-expanded={expanded}\n      className={cn(\"pointer-events-none z-50\", regionClass(variant, position, contained), className)}\n      {...handlers}\n      {...props}\n    >\n      <LiveRegions latest={toasts[0]} />\n      <ToastList\n        visible={toasts.slice(0, Math.max(1, max ?? DEFAULT_MAX[variant]))}\n        variant={variant}\n        effect={effect}\n        isTop={position.startsWith(\"top\")}\n        expanded={expanded}\n        onDismiss={onDismiss}\n      />\n    </section>\n  )\n}\n\nexport {\n  Toaster,\n  toast,\n  toastStore,\n  deckPlacement,\n  resolveSwipe,\n  type ToasterProps,\n  type ToastAction,\n  type ToastEffect,\n  type ToastOptions,\n  type ToastPosition,\n  type ToastPromiseMessages,\n  type ToastRecord,\n  type ToastType,\n  type ToastVariant,\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/toast-item.tsx",
      "content": "import type * as React from \"react\"\nimport { useCallback, useEffect, useLayoutEffect, useRef } from \"react\"\nimport { animate, motion, useMotionValue, useReducedMotion, type PanInfo } from \"motion/react\"\nimport { SPRING_DEFAULT, projectMomentum } from \"@/registry/miseui/lib/motion\"\nimport { BAR_TONE_CLASS, ROW_CLASS, SURFACE_CLASS, ToastRowContent, type ToastVariant } from \"@/registry/miseui/ui/toast-row\"\nimport type { ToastRecord } from \"@/registry/miseui/ui/toast-store\"\nimport { cn } from \"@/registry/miseui/lib/utils\"\n\nexport type ToastEffect = \"spring\"\nexport type { ToastVariant }\n\n/** Past this share of its width, the projected rest point means \"gone\". */\nconst SWIPE_FRACTION = 0.5\nconst THROW_CLEARANCE_PX = 48\nconst QUIET_TWEEN = { duration: 0.15, ease: [0.2, 0, 0, 1] } as const\nconst INSTANT = { duration: 0 } as const\nconst ENTER_OFFSET_PX = { spring: 24, quiet: 8 }\nconst ENTER_SCALE = 0.96\n\ninterface SwipeGesture {\n  /** Drag offset in px. */\n  offset: number\n  /** Release velocity in px/s. */\n  velocity: number\n  /** Rendered toast width in px. */\n  width: number\n}\n\n/** -1 or 1 to throw the toast that way, 0 to let it settle back. A flick counts for where it was going, not where it let go. */\nexport function resolveSwipe({ offset, velocity, width }: SwipeGesture): -1 | 0 | 1 {\n  const projected = offset + projectMomentum(velocity)\n  if (Math.abs(projected) <= width * SWIPE_FRACTION) return 0\n  return projected > 0 ? 1 : -1\n}\n\nfunction useAutoDismiss(onExpire: () => void, duration: number, paused: boolean) {\n  const remaining = useRef(duration)\n  useEffect(() => {\n    remaining.current = duration\n  }, [duration])\n  useEffect(() => {\n    if (paused || !Number.isFinite(duration)) return\n    const startedAt = Date.now()\n    const timer = setTimeout(onExpire, remaining.current)\n    return () => {\n      clearTimeout(timer)\n      remaining.current -= Date.now() - startedAt\n    }\n  }, [paused, duration, onExpire])\n}\n\n/** Reports the row's natural height. First pass runs before paint, so the deck never shows a guessed height. */\nfunction useMeasuredRow(id: string, onMeasure: (id: string, height: number | undefined) => void) {\n  const rowRef = useRef<HTMLDivElement>(null)\n  useLayoutEffect(() => {\n    const row = rowRef.current\n    if (!row) return\n    onMeasure(id, row.offsetHeight)\n    const observer = new ResizeObserver(() => onMeasure(id, row.offsetHeight))\n    observer.observe(row)\n    return () => {\n      observer.disconnect()\n      onMeasure(id, undefined)\n    }\n  }, [id, onMeasure])\n  return rowRef\n}\n\nfunction useSwipeAway(onGone: () => void) {\n  const x = useMotionValue(0)\n  const onDragEnd = (event: PointerEvent | MouseEvent | TouchEvent, info: PanInfo) => {\n    const card = event.target instanceof Element ? event.target.closest(\"[data-slot='toast']\") : null\n    const width = card?.getBoundingClientRect().width ?? 0\n    const side = resolveSwipe({ offset: info.offset.x, velocity: info.velocity.x, width })\n    if (side === 0) return\n    // Hand the release velocity to the spring so the throw has no seam, then remove it once it is off the edge.\n    animate(x, side * (width + THROW_CLEARANCE_PX), { ...SPRING_DEFAULT, velocity: info.velocity.x }).then(onGone)\n  }\n  return { x, onDragEnd }\n}\n\nexport interface ToastPlacement {\n  y: number\n  scale: number\n  height?: number\n  hideContent: boolean\n  zIndex: number\n}\n\ninterface ToastItemProps {\n  /** AnimatePresence popLayout has to reach the element it pops out of flow. */\n  ref?: React.Ref<HTMLLIElement>\n  record: ToastRecord\n  variant: ToastVariant\n  effect: ToastEffect | undefined\n  /** 1 when the toaster sits at the top, -1 at the bottom. */\n  direction: 1 | -1\n  paused: boolean\n  /** Deck position for `stack`; flow variants leave it out and reflow on their own. */\n  placement?: ToastPlacement\n  onDismiss: (id: string) => void\n  onExpire: (id: string) => void\n  onMeasure: (id: string, height: number | undefined) => void\n}\n\nfunction enterFrom(reduceMotion: boolean, effect: ToastEffect | undefined, direction: 1 | -1, restY: number) {\n  if (reduceMotion) return { opacity: 0, y: restY }\n  const distance = effect === \"spring\" ? ENTER_OFFSET_PX.spring : ENTER_OFFSET_PX.quiet\n  return { opacity: 0, y: restY - distance * direction, scale: ENTER_SCALE }\n}\n\nfunction ToastItem(props: ToastItemProps) {\n  const { ref, record, variant, effect, direction, paused, placement, onDismiss, onExpire, onMeasure } = props\n  const { id } = record\n  const reduceMotion = useReducedMotion() ?? false\n  const expire = useCallback(() => onExpire(id), [onExpire, id])\n  useAutoDismiss(expire, record.duration, paused)\n  const rowRef = useMeasuredRow(id, onMeasure)\n  const { x, onDragEnd } = useSwipeAway(() => onDismiss(id))\n  const transition = reduceMotion ? INSTANT : effect === \"spring\" ? SPRING_DEFAULT : QUIET_TWEEN\n  const restY = placement?.y ?? 0\n  return (\n    <motion.li\n      ref={ref}\n      layout={placement ? false : \"position\"}\n      data-slot=\"toast\"\n      data-type={record.type}\n      initial={enterFrom(reduceMotion, effect, direction, restY)}\n      animate={{ opacity: 1, y: restY, scale: placement?.scale ?? 1, ...(placement?.height ? { height: placement.height } : {}) }}\n      exit={reduceMotion ? { opacity: 0 } : { opacity: 0, scale: ENTER_SCALE }}\n      transition={transition}\n      style={{ x, zIndex: placement?.zIndex, transformOrigin: direction === -1 ? \"bottom center\" : \"top center\" }}\n      drag={effect === \"spring\" && !reduceMotion ? \"x\" : false}\n      dragSnapToOrigin\n      dragElastic={0.6}\n      dragMomentum={false}\n      onDragEnd={onDragEnd}\n      onKeyDown={(event) => {\n        if (event.key === \"Escape\") onDismiss(id)\n      }}\n      className={cn(\n        \"pointer-events-auto overflow-hidden\",\n        SURFACE_CLASS[variant],\n        variant === \"bar\" && BAR_TONE_CLASS[record.type],\n        placement && (direction === -1 ? \"absolute inset-x-0 bottom-0\" : \"absolute inset-x-0 top-0\"),\n      )}\n    >\n      <motion.div\n        ref={rowRef}\n        // Cards at the back of the deck are clipped, so their buttons must not take focus until the deck fans out.\n        inert={placement?.hideContent ?? false}\n        initial={false}\n        animate={{ opacity: placement?.hideContent ? 0 : 1 }}\n        transition={transition}\n        className={ROW_CLASS[variant]}\n      >\n        <ToastRowContent record={record} variant={variant} onDismiss={onDismiss} />\n      </motion.div>\n    </motion.li>\n  )\n}\n\nexport { ToastItem }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/toast-row.tsx",
      "content": "import { Check, CircleAlert, Loader2, X } from \"lucide-react\"\nimport type { ToastRecord, ToastType } from \"@/registry/miseui/ui/toast-store\"\nimport { cn } from \"@/registry/miseui/lib/utils\"\n\nexport type ToastVariant = \"stack\" | \"list\" | \"pill\" | \"bar\"\n\nexport const SURFACE_CLASS: Record<ToastVariant, string> = {\n  stack: \"rounded-lg border border-border bg-popover text-popover-foreground shadow-lg\",\n  list: \"rounded-lg border border-border bg-popover text-popover-foreground shadow-md\",\n  pill: \"rounded-full bg-foreground text-background shadow-lg\",\n  bar: \"w-full\",\n}\n\nexport const BAR_TONE_CLASS: Record<ToastType, string> = {\n  neutral: \"bg-foreground text-background\",\n  loading: \"bg-foreground text-background\",\n  success: \"bg-mise text-mise-foreground\",\n  error: \"bg-destructive text-white\",\n}\n\nexport const ROW_CLASS: Record<ToastVariant, string> = {\n  stack: \"flex items-start gap-3 p-3\",\n  list: \"flex items-start gap-3 p-3\",\n  pill: \"flex min-h-10 items-center gap-2 py-1 pr-1 pl-4\",\n  bar: \"mx-auto flex w-full max-w-5xl items-center gap-3 px-4 py-2\",\n}\n\nconst FOCUS_CLASS =\n  \"outline-none transition-colors duration-(--duration-fast) ease-(--ease-mise) focus-visible:ring-[3px] focus-visible:ring-ring/50\"\n\n/** `pill` and `bar` carry their own colour, so their controls inherit it instead of sitting on a popover. */\nconst isOnSurface = (variant: ToastVariant) => variant === \"stack\" || variant === \"list\"\n\nfunction TypeIcon({ type, variant }: { type: ToastType; variant: ToastVariant }) {\n  if (type === \"neutral\") return null\n  if (type === \"loading\") {\n    return <Loader2 aria-hidden=\"true\" className=\"size-5 shrink-0 animate-spin motion-reduce:animate-none\" />\n  }\n  const Icon = type === \"success\" ? Check : CircleAlert\n  const tone = type === \"success\" ? \"bg-mise text-mise-foreground\" : \"bg-destructive text-white\"\n  return (\n    <span\n      aria-hidden=\"true\"\n      className={cn(\"inline-flex size-5 shrink-0 items-center justify-center rounded-full\", isOnSurface(variant) && tone)}\n    >\n      <Icon className=\"size-3.5\" />\n    </span>\n  )\n}\n\nfunction ActionButton({ record, variant, onDismiss }: ToastRowProps) {\n  if (!record.action) return null\n  const { label, onClick } = record.action\n  return (\n    <button\n      type=\"button\"\n      onClick={() => {\n        onClick()\n        onDismiss(record.id)\n      }}\n      className={cn(\n        \"inline-flex h-10 shrink-0 items-center rounded-md px-3 text-[13px] font-medium whitespace-nowrap pointer-fine:h-8\",\n        FOCUS_CLASS,\n        isOnSurface(variant) ? \"border border-border hover:bg-accent\" : \"underline underline-offset-2 hover:opacity-80\",\n      )}\n    >\n      {label}\n    </button>\n  )\n}\n\ninterface ToastRowProps {\n  record: ToastRecord\n  variant: ToastVariant\n  onDismiss: (id: string) => void\n}\n\nexport function ToastRowContent({ record, variant, onDismiss }: ToastRowProps) {\n  const descriptionTone = isOnSurface(variant) ? \"text-muted-foreground\" : \"opacity-80\"\n  return (\n    <>\n      <TypeIcon type={record.type} variant={variant} />\n      <div className=\"min-w-0 flex-1\">\n        <p className={cn(\"text-sm font-medium\", variant === \"pill\" && \"truncate\")}>{record.title}</p>\n        {record.description && variant !== \"pill\" && (\n          <p className={cn(\"text-[13px]/5\", descriptionTone, variant !== \"bar\" && \"mt-1\")}>{record.description}</p>\n        )}\n      </div>\n      <ActionButton record={record} variant={variant} onDismiss={onDismiss} />\n      <button\n        type=\"button\"\n        aria-label={`Dismiss: ${record.title}`}\n        onClick={() => onDismiss(record.id)}\n        className={cn(\n          \"inline-flex size-10 shrink-0 items-center justify-center rounded-md pointer-fine:size-7\",\n          FOCUS_CLASS,\n          isOnSurface(variant) ? \"text-muted-foreground hover:bg-accent hover:text-accent-foreground\" : \"hover:opacity-70\",\n          variant === \"pill\" && \"rounded-full\",\n        )}\n      >\n        <X aria-hidden=\"true\" className=\"size-4\" />\n      </button>\n    </>\n  )\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/toast-layout.ts",
      "content": "import type { ToastPlacement, ToastVariant } from \"@/registry/miseui/ui/toast-item\"\nimport { cn } from \"@/registry/miseui/lib/utils\"\n\nexport type ToastPosition = \"top-left\" | \"top-center\" | \"top-right\" | \"bottom-left\" | \"bottom-center\" | \"bottom-right\"\n\n/** Used until a card reports its height; close to a one-line toast so the first fan-out does not jump far. */\nexport const FALLBACK_HEIGHT_PX = 64\nexport const DECK_GAP_PX = 8\nconst DECK_PEEK_PX = 8\nconst DECK_SCALE_STEP = 0.05\n\nexport const DEFAULT_MAX: Record<ToastVariant, number> = { stack: 3, list: 4, pill: 1, bar: 1 }\n\nconst CARD_POSITION: Record<ToastPosition, string> = {\n  \"top-left\": \"top-4 left-4\",\n  \"top-center\": \"top-4 left-1/2 -translate-x-1/2\",\n  \"top-right\": \"top-4 right-4\",\n  \"bottom-left\": \"bottom-4 left-4\",\n  \"bottom-center\": \"bottom-4 left-1/2 -translate-x-1/2\",\n  \"bottom-right\": \"bottom-4 right-4\",\n}\n\ninterface DeckInput {\n  index: number\n  expanded: boolean\n  /** Heights of the visible toasts, newest first. */\n  heights: readonly number[]\n  /** 1 when the toaster sits at the top, -1 at the bottom. */\n  direction: 1 | -1\n}\n\n/** Where one card sits: a peeking deck at rest, a plain column when fanned out. */\nexport function deckPlacement({ index, expanded, heights, direction }: DeckInput): ToastPlacement {\n  const zIndex = heights.length - index\n  if (expanded) {\n    const distance = heights.slice(0, index).reduce((sum, height) => sum + height + DECK_GAP_PX, 0)\n    return { y: distance * direction, scale: 1, height: heights[index], hideContent: false, zIndex }\n  }\n  return {\n    y: index * DECK_PEEK_PX * direction,\n    scale: 1 - index * DECK_SCALE_STEP,\n    // Back cards take the front card's height, so a long toast behind a short one does not stick out of the deck.\n    height: heights[0],\n    hideContent: index > 0,\n    zIndex,\n  }\n}\n\nexport function regionClass(variant: ToastVariant, position: ToastPosition, contained: boolean) {\n  const isTop = position.startsWith(\"top\")\n  const anchor = contained ? \"absolute\" : \"fixed\"\n  if (variant === \"bar\") return cn(anchor, \"inset-x-0\", isTop ? \"top-0\" : \"bottom-0\")\n  const edge = contained ? (isTop ? \"top-2\" : \"bottom-2\") : isTop ? \"top-4\" : \"bottom-4\"\n  if (variant === \"pill\") return cn(anchor, edge, \"left-1/2 w-max max-w-[calc(100%-1rem)] -translate-x-1/2\")\n  // A preview is too narrow for corners to mean anything, so contained cards centre.\n  if (contained) return cn(anchor, edge, \"left-1/2 w-[calc(100%-1rem)] max-w-[22rem] -translate-x-1/2\")\n  return cn(anchor, CARD_POSITION[position], \"w-[min(22rem,calc(100vw-2rem))]\")\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/miseui/ui/toast-store.ts",
      "content": "export type ToastType = \"neutral\" | \"success\" | \"error\" | \"loading\"\n\nexport interface ToastAction {\n  label: string\n  onClick: () => void\n}\n\nexport interface ToastOptions {\n  /** Reuse an id to update a toast in place, as the promise helper does. */\n  id?: string\n  description?: string\n  action?: ToastAction\n  /** Milliseconds on screen. Pass Infinity to keep it until dismissed. */\n  duration?: number\n  /** Which `<Toaster id>` shows it, when a page has more than one. */\n  toaster?: string\n}\n\nexport interface ToastRecord {\n  id: string\n  type: ToastType\n  title: string\n  description?: string\n  action?: ToastAction\n  duration: number\n  toaster: string\n}\n\nexport interface ToastPromiseMessages<T> {\n  loading: string\n  success: string | ((value: T) => string)\n  error: string | ((reason: unknown) => string)\n}\n\nexport const DEFAULT_TOASTER = \"default\"\n\n/** Errors stay longer because they usually need reading; loading stays until its work settles. */\nconst DEFAULT_DURATION_MS: Record<ToastType, number> = {\n  neutral: 4000,\n  success: 4000,\n  error: 6000,\n  loading: Number.POSITIVE_INFINITY,\n}\n/** A toast with a button needs time for someone to reach it. */\nconst ACTION_MIN_DURATION_MS = 8000\n\nconst EMPTY: readonly ToastRecord[] = []\n\nlet records: readonly ToastRecord[] = EMPTY\nlet nextId = 1\nconst listeners = new Set<() => void>()\n\nfunction commit(next: readonly ToastRecord[]) {\n  records = next\n  listeners.forEach((listener) => listener())\n}\n\nexport const toastStore = {\n  subscribe(listener: () => void) {\n    listeners.add(listener)\n    return () => {\n      listeners.delete(listener)\n    }\n  },\n  getSnapshot: () => records,\n  getServerSnapshot: () => EMPTY,\n}\n\nfunction resolveDuration(type: ToastType, options: ToastOptions): number {\n  if (options.duration !== undefined) return options.duration\n  const base = DEFAULT_DURATION_MS[type]\n  return options.action ? Math.max(base, ACTION_MIN_DURATION_MS) : base\n}\n\nfunction show(type: ToastType, title: string, options: ToastOptions = {}): string {\n  const id = options.id ?? `toast-${nextId++}`\n  const record: ToastRecord = {\n    id,\n    type,\n    title,\n    description: options.description,\n    action: options.action,\n    duration: resolveDuration(type, options),\n    toaster: options.toaster ?? DEFAULT_TOASTER,\n  }\n  const exists = records.some((entry) => entry.id === id)\n  commit(exists ? records.map((entry) => (entry.id === id ? record : entry)) : [record, ...records])\n  return id\n}\n\nfunction dismiss(id?: string) {\n  commit(id === undefined ? EMPTY : records.filter((entry) => entry.id !== id))\n}\n\nfunction resolveMessage<V>(message: string | ((value: V) => string), value: V): string {\n  return typeof message === \"function\" ? message(value) : message\n}\n\nfunction promiseToast<T>(\n  work: Promise<T> | (() => Promise<T>),\n  messages: ToastPromiseMessages<T>,\n  options: Omit<ToastOptions, \"id\" | \"duration\"> = {},\n): Promise<T> {\n  const pending = typeof work === \"function\" ? work() : work\n  const id = show(\"loading\", messages.loading, options)\n  // Both handlers live here, so a rejection shown as a toast does not also surface as an unhandled rejection.\n  pending.then(\n    (value) => show(\"success\", resolveMessage(messages.success, value), { ...options, id }),\n    (reason: unknown) => show(\"error\", resolveMessage(messages.error, reason), { ...options, id }),\n  )\n  return pending\n}\n\nexport const toast = Object.assign((title: string, options?: ToastOptions) => show(\"neutral\", title, options), {\n  success: (title: string, options?: ToastOptions) => show(\"success\", title, options),\n  error: (title: string, options?: ToastOptions) => show(\"error\", title, options),\n  loading: (title: string, options?: ToastOptions) => show(\"loading\", title, options),\n  promise: promiseToast,\n  dismiss,\n})\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "feedback"
  ],
  "type": "registry:ui"
}