{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "toast",
  "type": "registry:ui",
  "title": "Toast",
  "description": "Brief notifications from anywhere with toast(): success, error, warning, info, loading and promise, with actions and swipe to dismiss.",
  "dependencies": [
    "tailwind-variants@^1.0.0"
  ],
  "registryDependencies": [
    "https://junco.madebytobi.com/r/icons.json",
    "https://junco.madebytobi.com/r/junco-theme.json",
    "https://junco.madebytobi.com/r/spinner.json",
    "https://junco.madebytobi.com/r/utils.json"
  ],
  "files": [
    {
      "path": "registry/junco/ui/toast.tsx",
      "type": "registry:ui",
      "content": "import * as React from \"react\";\nimport { tv } from \"tailwind-variants\";\nimport { cn } from \"@/lib/utils\";\nimport { CircleCheckIcon, CircleXIcon, InfoIcon, TriangleAlertIcon, XIcon } from \"@/lib/junco/icons\";\nimport { Spinner } from \"@/components/ui/spinner\";\n\n/* ------------------------------------------------------------------------------------------------\n * Store: a tiny module-level queue so `toast()` works from anywhere, no hook or provider needed.\n * ----------------------------------------------------------------------------------------------*/\n\nexport type ToastType = \"default\" | \"success\" | \"error\" | \"warning\" | \"info\" | \"loading\";\nexport type ToastId = string | number;\n\nexport interface ToastAction {\n  label: React.ReactNode;\n  /** Runs, then the toast closes. Call `event.preventDefault()` to keep it open. */\n  onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;\n}\n\nexport interface ToastOptions {\n  /** Reuse an id to update a toast in place instead of adding a new one. */\n  id?: ToastId;\n  description?: React.ReactNode;\n  /** Milliseconds before it closes. `Infinity` keeps it open. Loading toasts never auto-close. */\n  duration?: number;\n  action?: ToastAction;\n  /** Whether the user can close it (close button, swipe, Esc). Default true. */\n  dismissible?: boolean;\n  /** Text for screen readers when title/description aren't plain strings. */\n  ariaLabel?: string;\n  onDismiss?: (toast: ToastData) => void;\n  onAutoClose?: (toast: ToastData) => void;\n  className?: string;\n}\n\nexport interface ToastData extends ToastOptions {\n  id: ToastId;\n  type: ToastType;\n  title: React.ReactNode;\n  /** Bumps on every update so timers restart. */\n  version: number;\n  dismissed: boolean;\n}\n\ntype Listener = () => void;\n\nconst EXIT_MS = 200;\nlet counter = 0;\nlet toasts: ToastData[] = [];\nconst listeners = new Set<Listener>();\n\nfunction emit() {\n  toasts = [...toasts];\n  for (const l of listeners) l();\n}\n\nfunction upsert(type: ToastType, title: React.ReactNode, options: ToastOptions = {}): ToastId {\n  const existing = options.id !== undefined ? toasts.find((t) => t.id === options.id) : undefined;\n  if (existing) {\n    toasts = toasts.map((t) =>\n      t.id === existing.id ? { ...t, ...options, type, title, version: t.version + 1, dismissed: false } : t,\n    );\n  } else {\n    const id = options.id ?? ++counter;\n    toasts = [{ ...options, id, type, title, version: 0, dismissed: false }, ...toasts];\n  }\n  emit();\n  return options.id ?? toasts[0]!.id;\n}\n\nfunction remove(id: ToastId) {\n  toasts = toasts.filter((t) => t.id !== id);\n  emit();\n}\n\nfunction dismiss(id?: ToastId, reason: \"dismiss\" | \"auto\" = \"dismiss\") {\n  const targets = toasts.filter((t) => (id === undefined || t.id === id) && !t.dismissed);\n  if (!targets.length) return;\n  toasts = toasts.map((t) => (targets.includes(t) ? { ...t, dismissed: true } : t));\n  emit();\n  for (const t of targets) {\n    if (reason === \"auto\") t.onAutoClose?.(t);\n    else t.onDismiss?.(t);\n    setTimeout(() => remove(t.id), EXIT_MS);\n  }\n}\n\ntype PromiseMessage<T> = React.ReactNode | ((value: T) => React.ReactNode);\n\nexport interface ToastPromiseOptions<T> extends Omit<ToastOptions, \"id\"> {\n  loading: React.ReactNode;\n  success: PromiseMessage<T>;\n  error: PromiseMessage<unknown>;\n}\n\nfunction resolveMessage<T>(message: PromiseMessage<T>, value: T): React.ReactNode {\n  return typeof message === \"function\" ? (message as (v: T) => React.ReactNode)(value) : message;\n}\n\n/**\n * Brief notifications from anywhere with toast(): success, error, warning, info, loading and promise, with actions and swipe to dismiss.\n *\n *   toast(\"Saved\")\n *   toast.success(\"Invoice sent\", { description: \"Northwind will get it in a minute.\" })\n *   toast.error(\"Couldn't save\", { action: { label: \"Retry\", onClick: save } })\n *   toast.promise(upload(), { loading: \"Uploading…\", success: \"Uploaded\", error: \"Upload failed\" })\n *   toast.dismiss(id)\n */\nexport const toast = Object.assign((title: React.ReactNode, options?: ToastOptions) => upsert(\"default\", title, options), {\n  success: (title: React.ReactNode, options?: ToastOptions) => upsert(\"success\", title, options),\n  error: (title: React.ReactNode, options?: ToastOptions) => upsert(\"error\", title, options),\n  warning: (title: React.ReactNode, options?: ToastOptions) => upsert(\"warning\", title, options),\n  info: (title: React.ReactNode, options?: ToastOptions) => upsert(\"info\", title, options),\n  loading: (title: React.ReactNode, options?: ToastOptions) => upsert(\"loading\", title, options),\n  /** Shows a loading toast that turns into success or error. Returns the original promise. */\n  promise<T>(promise: Promise<T> | (() => Promise<T>), { loading, success, error, ...options }: ToastPromiseOptions<T>) {\n    const id = upsert(\"loading\", loading, options);\n    const p = typeof promise === \"function\" ? promise() : promise;\n    p.then(\n      (value) => upsert(\"success\", resolveMessage(success, value), { ...options, id }),\n      (reason: unknown) => upsert(\"error\", resolveMessage(error, reason), { ...options, id }),\n    );\n    return p;\n  },\n  dismiss: (id?: ToastId) => dismiss(id),\n});\n\n/** @internal Clears every toast immediately. For tests. */\nexport function __resetToasts() {\n  toasts = [];\n  emit();\n}\n\nfunction subscribe(listener: Listener) {\n  listeners.add(listener);\n  return () => listeners.delete(listener);\n}\nconst getSnapshot = () => toasts;\n\n/** Read the current toasts, e.g. to build a custom toaster. */\nexport function useToasts(): ToastData[] {\n  return React.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);\n}\n\n/* ------------------------------------------------------------------------------------------------\n * Toaster\n * ----------------------------------------------------------------------------------------------*/\n\nexport type ToasterPosition = \"top-left\" | \"top-center\" | \"top-right\" | \"bottom-left\" | \"bottom-center\" | \"bottom-right\";\n\nexport const toastVariants = tv({\n  slots: {\n    viewport: \"fixed z-toast w-[min(24rem,calc(100vw-2rem))] outline-none\",\n    list: \"relative m-0 list-none p-0 transition-[height] duration-slow ease-standard\",\n    item: [\n      \"absolute inset-x-0 transition-[transform,opacity] duration-slow ease-standard outline-none\",\n      \"animate-toast-in data-[dismissed=true]:animate-toast-out data-[dismissed=true]:pointer-events-none\",\n    ],\n    toast: [\n      \"flex items-start gap-3 rounded-card border border-border bg-popover p-4 text-popover-foreground shadow-float\",\n      \"touch-pan-y transition-[translate,opacity] duration-base ease-standard\",\n    ],\n    icon: \"mt-px shrink-0 [&_svg]:size-4\",\n    content: \"grid min-w-0 flex-1 gap-0.5\",\n    title: \"text-sm leading-5 font-medium\",\n    description: \"text-sm leading-5 text-muted-foreground\",\n    action: [\n      \"ml-1 inline-flex h-7 shrink-0 items-center rounded-control bg-primary px-2.5 text-xs font-medium text-primary-foreground\",\n      \"transition-colors duration-fast ease-standard hover:bg-primary/90\",\n      \"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring\",\n    ],\n    close: [\n      \"-mt-1 -mr-1 grid size-6 shrink-0 place-items-center rounded-control text-muted-foreground\",\n      \"transition-colors duration-fast ease-standard hover:bg-accent hover:text-foreground\",\n      \"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring\",\n    ],\n  },\n  variants: {\n    type: {\n      default: { icon: \"hidden\" },\n      success: { icon: \"text-success-text\" },\n      error: { icon: \"text-destructive-text\" },\n      warning: { icon: \"text-warning-text\" },\n      info: { icon: \"text-primary\" },\n      loading: { icon: \"text-muted-foreground\" },\n    },\n  },\n});\n\nconst icons: Record<ToastType, React.ReactNode> = {\n  default: null,\n  success: <CircleCheckIcon />,\n  error: <CircleXIcon />,\n  warning: <TriangleAlertIcon />,\n  info: <InfoIcon />,\n  loading: <Spinner label={null} size=\"sm\" className=\"size-4\" />,\n};\n\n/** Words read before the message, so the type never depends on the icon color. */\nconst typeWords: Record<ToastType, string> = {\n  default: \"\",\n  success: \"Success\",\n  error: \"Error\",\n  warning: \"Warning\",\n  info: \"Info\",\n  loading: \"Loading\",\n};\n\nconst positions: Record<ToasterPosition, 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\nexport interface ToasterProps {\n  position?: ToasterPosition;\n  /** How many toasts show at once; older ones wait behind. Default 3. */\n  visibleToasts?: number;\n  /** Default auto-close time in ms. Default 5000. */\n  duration?: number;\n  /** Always show the full stack instead of collapsing it until hover or focus. */\n  expand?: boolean;\n  /** Show the × button. Default true. */\n  closeButton?: boolean;\n  /** Keys that move focus to the toasts. Default Alt+T. */\n  hotkey?: string[];\n  gap?: number;\n  className?: string;\n  classNames?: Partial<Record<keyof typeof toastVariants.slots, string>>;\n}\n\nfunction plainText(node: React.ReactNode): string {\n  return typeof node === \"string\" || typeof node === \"number\" ? String(node) : \"\";\n}\n\n/**\n * Put one <Toaster /> near the root of your app, then call `toast()` anywhere. */\nexport function Toaster({\n  position = \"bottom-right\",\n  visibleToasts = 3,\n  duration = 5000,\n  expand = false,\n  closeButton = true,\n  hotkey = [\"altKey\", \"KeyT\"],\n  gap = 12,\n  className,\n  classNames,\n}: ToasterProps) {\n  const all = useToasts();\n  const [heights, setHeights] = React.useState<Record<string, number>>({});\n  const [hovered, setHovered] = React.useState(false);\n  const [focused, setFocused] = React.useState(false);\n  const [announcement, setAnnouncement] = React.useState({ polite: \"\", assertive: \"\" });\n  const viewportRef = React.useRef<HTMLElement>(null);\n  const announced = React.useRef(new Map<ToastId, number>());\n\n  const expanded = expand || hovered || focused;\n  const isTop = position.startsWith(\"top\");\n  const align = position.endsWith(\"left\") ? \"left\" : position.endsWith(\"right\") ? \"right\" : \"center\";\n\n  // Announce each new toast or update once, through live regions that are always mounted.\n  React.useEffect(() => {\n    for (const t of all) {\n      if (t.dismissed || announced.current.get(t.id) === t.version) continue;\n      announced.current.set(t.id, t.version);\n      const text =\n        t.ariaLabel ?? [typeWords[t.type], plainText(t.title), plainText(t.description)].filter(Boolean).join(\": \");\n      if (text) {\n        setAnnouncement((a) =>\n          t.type === \"error\" ? { ...a, assertive: text } : { ...a, polite: text },\n        );\n      }\n    }\n    for (const id of announced.current.keys()) if (!all.some((t) => t.id === id)) announced.current.delete(id);\n  }, [all]);\n\n  React.useEffect(() => {\n    const onKeyDown = (e: KeyboardEvent) => {\n      const match = hotkey.every((key) => (e as unknown as Record<string, unknown>)[key] === true || e.code === key);\n      if (match) {\n        e.preventDefault();\n        const first = viewportRef.current?.querySelector<HTMLElement>('li[tabindex=\"0\"]');\n        (first ?? viewportRef.current)?.focus();\n      }\n    };\n    document.addEventListener(\"keydown\", onKeyDown);\n    return () => document.removeEventListener(\"keydown\", onKeyDown);\n  }, [hotkey]);\n\n  const setHeight = React.useCallback((id: ToastId, h: number) => {\n    setHeights((prev) => (prev[String(id)] === h ? prev : { ...prev, [String(id)]: h }));\n  }, []);\n\n  const live = all.filter((t) => !t.dismissed);\n  const styles = toastVariants();\n  const frontHeight = heights[String(live[0]?.id)] ?? 0;\n  const shown = live.slice(0, visibleToasts);\n  const listHeight = expanded\n    ? shown.reduce((sum, t) => sum + (heights[String(t.id)] ?? 0), 0) + gap * Math.max(0, shown.length - 1)\n    : frontHeight + Math.min(shown.length - 1, 2) * 10;\n\n  return (\n    <section\n      ref={viewportRef}\n      aria-label={`Notifications (${hotkey.map((k) => k.replace(/^Key/, \"\").replace(/Key$/, \"\")).join(\"+\")})`}\n      tabIndex={-1}\n      className={styles.viewport({ className: cn(positions[position], classNames?.viewport, className) })}\n      onMouseEnter={() => setHovered(true)}\n      onMouseLeave={() => setHovered(false)}\n      onFocus={() => setFocused(true)}\n      onBlur={(e) => {\n        if (!e.currentTarget.contains(e.relatedTarget as Node | null)) setFocused(false);\n      }}\n    >\n      <span aria-live=\"polite\" aria-atomic=\"true\" className=\"sr-only\">\n        {announcement.polite}\n      </span>\n      <span aria-live=\"assertive\" aria-atomic=\"true\" className=\"sr-only\">\n        {announcement.assertive}\n      </span>\n      <ol className={styles.list({ className: classNames?.list })} style={{ height: listHeight }}>\n        {all.map((t) => {\n          // A closing toast keeps its slot while it fades out.\n          const order = t.dismissed ? all.filter((x) => !x.dismissed || x === t) : live;\n          const index = order.indexOf(t);\n          const newer = order.slice(0, index);\n          const offset = expanded\n            ? newer.reduce((sum, n) => sum + (heights[String(n.id)] ?? 0), 0) + gap * newer.length\n            : Math.min(index, 2) * 10;\n          const hidden = index >= visibleToasts;\n          return (\n            <ToastItem\n              key={t.id}\n              toast={t}\n              index={index}\n              offset={offset}\n              isTop={isTop}\n              align={align}\n              expanded={expanded}\n              hidden={hidden}\n              frontHeight={frontHeight}\n              duration={duration}\n              paused={expanded}\n              closeButton={closeButton}\n              classNames={classNames}\n              onHeight={setHeight}\n            />\n          );\n        })}\n      </ol>\n    </section>\n  );\n}\n\ninterface ToastItemProps {\n  toast: ToastData;\n  index: number;\n  offset: number;\n  isTop: boolean;\n  align: \"left\" | \"right\" | \"center\";\n  expanded: boolean;\n  hidden: boolean;\n  frontHeight: number;\n  duration: number;\n  paused: boolean;\n  closeButton: boolean;\n  classNames?: ToasterProps[\"classNames\"];\n  onHeight: (id: ToastId, height: number) => void;\n}\n\nfunction ToastItem({\n  toast: t,\n  index,\n  offset,\n  isTop,\n  align,\n  expanded,\n  hidden,\n  frontHeight,\n  duration,\n  paused,\n  closeButton,\n  classNames,\n  onHeight,\n}: ToastItemProps) {\n  const ref = React.useRef<HTMLDivElement>(null);\n  const swipe = React.useRef<{ x: number; t: number; dx: number } | null>(null);\n  const [swipeX, setSwipeX] = React.useState(0);\n  const [documentHidden, setDocumentHidden] = React.useState(false);\n  const styles = toastVariants({ type: t.type });\n  const dismissible = t.dismissible !== false;\n\n  React.useLayoutEffect(() => {\n    const el = ref.current;\n    if (!el) return;\n    onHeight(t.id, el.offsetHeight);\n    if (typeof ResizeObserver === \"undefined\") return;\n    const ro = new ResizeObserver(() => onHeight(t.id, el.offsetHeight));\n    ro.observe(el);\n    return () => ro.disconnect();\n  }, [t.id, onHeight]);\n\n  React.useEffect(() => {\n    const onVisibility = () => setDocumentHidden(document.hidden);\n    document.addEventListener(\"visibilitychange\", onVisibility);\n    return () => document.removeEventListener(\"visibilitychange\", onVisibility);\n  }, []);\n\n  // Auto-close, pausing while the stack is hovered or focused, or the tab is hidden.\n  const remaining = React.useRef(0);\n  React.useEffect(() => {\n    remaining.current = t.duration ?? duration;\n  }, [t.version, t.duration, duration]);\n  React.useEffect(() => {\n    if (t.type === \"loading\" || t.dismissed || paused || documentHidden) return;\n    const ms = remaining.current;\n    if (!Number.isFinite(ms)) return;\n    const started = Date.now();\n    const timer = setTimeout(() => dismiss(t.id, \"auto\"), ms);\n    return () => {\n      clearTimeout(timer);\n      remaining.current = Math.max(0, ms - (Date.now() - started));\n    };\n  }, [t.id, t.type, t.version, t.dismissed, paused, documentHidden]);\n\n  const onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {\n    if (!dismissible || (e.target as HTMLElement).closest(\"button\")) return;\n    e.currentTarget.setPointerCapture(e.pointerId);\n    swipe.current = { x: e.clientX, t: performance.now(), dx: 0 };\n  };\n  const onPointerMove = (e: React.PointerEvent<HTMLDivElement>) => {\n    if (!swipe.current) return;\n    let dx = e.clientX - swipe.current.x;\n    if (align === \"right\") dx = Math.max(0, dx);\n    if (align === \"left\") dx = Math.min(0, dx);\n    swipe.current.dx = dx;\n    setSwipeX(dx);\n  };\n  const onPointerUp = () => {\n    const s = swipe.current;\n    swipe.current = null;\n    if (!s) return;\n    const velocity = Math.abs(s.dx) / Math.max(1, performance.now() - s.t);\n    if (Math.abs(s.dx) > 60 || velocity > 0.4) {\n      setSwipeX(Math.sign(s.dx || 1) * (ref.current?.offsetWidth ?? 400));\n      dismiss(t.id);\n    } else {\n      setSwipeX(0);\n    }\n  };\n\n  const isFront = index === 0;\n  const behind = !expanded && index > 0;\n  const scale = behind ? 1 - Math.min(index, 2) * 0.05 : 1;\n  const y = isTop ? offset : -offset;\n\n  return (\n    <li\n      tabIndex={hidden ? -1 : 0}\n      aria-hidden={hidden || undefined}\n      data-type={t.type}\n      data-dismissed={t.dismissed}\n      data-front={isFront || undefined}\n      className={styles.item({ className: cn(isTop ? \"top-0 [--toast-from:-100%]\" : \"bottom-0 [--toast-from:100%]\", t.className) })}\n      style={{\n        transform: `translateY(${y}px) scale(${scale})`,\n        transformOrigin: isTop ? \"bottom center\" : \"top center\",\n        opacity: hidden ? 0 : 1,\n        pointerEvents: hidden ? \"none\" : undefined,\n        zIndex: 100 - Math.max(0, index),\n      }}\n      onKeyDown={(e) => {\n        if (e.key === \"Escape\" && dismissible) {\n          e.stopPropagation();\n          dismiss(t.id);\n        }\n      }}\n    >\n      <div\n        ref={ref}\n        className={styles.toast({ className: classNames?.toast })}\n        style={{\n          translate: swipeX ? `${swipeX}px 0` : undefined,\n          opacity: swipeX ? Math.max(0, 1 - Math.abs(swipeX) / 200) : undefined,\n          transition: swipe.current ? \"none\" : undefined,\n          // Collapsed toasts behind the front one take its height and hide their text.\n          height: behind && frontHeight ? frontHeight : undefined,\n          overflow: behind ? \"hidden\" : undefined,\n        }}\n        onPointerDown={onPointerDown}\n        onPointerMove={onPointerMove}\n        onPointerUp={onPointerUp}\n        onPointerCancel={onPointerUp}\n      >\n        <span className={styles.icon({ className: cn(behind && \"invisible\", classNames?.icon) })} aria-hidden>\n          {icons[t.type]}\n        </span>\n        <div className={styles.content({ className: cn(behind && \"invisible\", classNames?.content) })}>\n          {t.type !== \"default\" && <span className=\"sr-only\">{typeWords[t.type]}: </span>}\n          <div className={styles.title({ className: classNames?.title })}>{t.title}</div>\n          {t.description && <div className={styles.description({ className: classNames?.description })}>{t.description}</div>}\n        </div>\n        {t.action && (\n          <button\n            type=\"button\"\n            className={styles.action({ className: cn(behind && \"invisible\", classNames?.action) })}\n            onClick={(e) => {\n              t.action?.onClick(e);\n              if (!e.defaultPrevented) dismiss(t.id);\n            }}\n          >\n            {t.action.label}\n          </button>\n        )}\n        {closeButton && dismissible && (\n          <button\n            type=\"button\"\n            aria-label=\"Dismiss notification\"\n            className={styles.close({ className: cn(behind && \"invisible\", classNames?.close) })}\n            onClick={() => dismiss(t.id)}\n          >\n            <XIcon className=\"size-3.5\" />\n          </button>\n        )}\n      </div>\n    </li>\n  );\n}\n"
    }
  ],
  "docs": "Docs and examples: https://junco.madebytobi.com/docs/components/toast",
  "meta": {
    "hash": "f08fdf571f0bb4eb",
    "source": "packages/react/src/components/toast.tsx",
    "exports": [
      "ToastType",
      "ToastId",
      "ToastAction",
      "ToastOptions",
      "ToastData",
      "ToastPromiseOptions",
      "toast",
      "__resetToasts",
      "useToasts",
      "ToasterPosition",
      "toastVariants",
      "ToasterProps",
      "Toaster"
    ],
    "props": {
      "Toaster": {
        "props": [
          {
            "name": "position",
            "optional": true,
            "type": "ToasterPosition"
          },
          {
            "name": "visibleToasts",
            "optional": true,
            "type": "number",
            "description": "How many toasts show at once; older ones wait behind. Default 3."
          },
          {
            "name": "duration",
            "optional": true,
            "type": "number",
            "description": "Default auto-close time in ms. Default 5000."
          },
          {
            "name": "expand",
            "optional": true,
            "type": "boolean",
            "description": "Always show the full stack instead of collapsing it until hover or focus."
          },
          {
            "name": "closeButton",
            "optional": true,
            "type": "boolean",
            "description": "Show the × button. Default true."
          },
          {
            "name": "hotkey",
            "optional": true,
            "type": "string[]",
            "description": "Keys that move focus to the toasts. Default Alt+T."
          },
          {
            "name": "gap",
            "optional": true,
            "type": "number"
          },
          {
            "name": "className",
            "optional": true,
            "type": "string"
          },
          {
            "name": "classNames",
            "optional": true,
            "type": "Partial<Record<keyof typeof toastVariants.slots"
          }
        ]
      }
    },
    "variants": {
      "variants": {
        "type": [
          "default",
          "success",
          "error",
          "warning",
          "info",
          "loading"
        ]
      },
      "defaultVariants": {}
    },
    "usage": "toast(\"Saved\")\ntoast.success(\"Invoice sent\", { description: \"Northwind will get it in a minute.\" })\ntoast.error(\"Couldn't save\", { action: { label: \"Retry\", onClick: save } })\ntoast.promise(upload(), { loading: \"Uploading…\", success: \"Uploaded\", error: \"Upload failed\" })\ntoast.dismiss(id)",
    "docsUrl": "https://junco.madebytobi.com/docs/components/toast"
  }
}
