# P toast 9

> Long-running promise toast with cancel

A shadcn/ui block from the [@coss](https://coss.com/ui) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/coss/p-toast-9).

- Page, with a live preview: https://shadcn.exchange/r/coss/p-toast-9
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/coss/p-toast-9-light-5bb46346dc4eb2c0.png
- Registry item: https://coss.com/ui/r/p-toast-9.json
- Categories: toast, button
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://coss.com/ui/r/p-toast-9.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `lucide-react`
- Registry items: `@coss/button`, `@coss/toast`

## Files

### `registry/default/particles/p-toast-9.tsx`

```tsx
"use client";

import { DownloadIcon } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { Button } from "@/registry/default/ui/button";
import { toastManager } from "@/registry/default/ui/toast";

export default function Particle() {
  const [isGenerating, setIsGenerating] = useState(false);
  const [progress, setProgress] = useState(0);
  const abortControllerRef = useRef<AbortController | null>(null);

  useEffect(() => {
    if (!isGenerating) return;

    const interval = setInterval(() => {
      setProgress((prev) =>
        Math.min(99, prev + Math.round(Math.random() * 8 + 2)),
      );
    }, 300);

    return () => clearInterval(interval);
  }, [isGenerating]);

  async function handleDownload() {
    if (isGenerating) return;

    setIsGenerating(true);
    setProgress(0);
    abortControllerRef.current = new AbortController();

    try {
      await toastManager.promise(
        new Promise<string>((resolve, reject) => {
          const shouldSucceed = Math.random() > 0.2;
          const timeoutId = setTimeout(() => {
            if (shouldSucceed) {
              resolve("Report ready");
            } else {
              reject(new Error("Generation failed"));
            }
          }, 4000);

          abortControllerRef.current?.signal.addEventListener("abort", () => {
            clearTimeout(timeoutId);
            reject(new DOMException("Cancelled", "AbortError"));
          });
        }),
        {
          error: (err: Error) => {
            if (err.name === "AbortError") {
              return {
                actionProps: undefined,
                description: "Report generation was cancelled.",
                title: "Cancelled",
                type: "info" as const,
              };
            }
            return {
              actionProps: undefined,
              description: "Please try again later.",
              title: "Failed to generate report",
            };
          },
          loading: {
            actionProps: {
              children: "Cancel",
              onClick: () => abortControllerRef.current?.abort(),
            },
            description: "Your download will begin once ready.",
            title: "Generating report…",
          },
          success: () => ({
            actionProps: undefined,
            description: "Your file is now downloading.",
            title: "Download started",
          }),
        },
      );
    } finally {
      setIsGenerating(false);
      setProgress(0);
      abortControllerRef.current = null;
    }
  }

  return (
    <Button disabled={isGenerating} onClick={handleDownload} variant="outline">
      {isGenerating ? (
        <>
          Loading…{" "}
          <span className="tabular-nums">
            {progress.toString().padStart(2, "\u2007")}%
          </span>
        </>
      ) : (
        <>
          <DownloadIcon />
          Download
        </>
      )}
    </Button>
  );
}
```
