Componentfeedback1.16 KB gzipZero dependencies

Upload Progress

Single file upload with circular progress ring and cancel.

Preview

Live Preview
65%

ticket.pdf

Uploading…

Installation

Tokens guide →

Add to your project via CLI (zero dependencies, code you own):

npx bigbullui add upload-progress

Or install the full npm package:

npm install bigbullui
import { UploadProgress } from "@/components/ui/upload-progress";

<UploadProgress />

Usage guidance

Use Upload Progress when a ticket stub surface needs Single file upload with circular progress ring and cancel. Keep props minimal and prefer composition with sibling stubs.

Avoid Upload Progress for long form flows or dense data grids where a dedicated layout block fits better.

Source code

upload-progress.tsx

Zero dependencies (only React + utils). Copy and paste directly into your project:

"use client";

import * as React from "react";
import { cn } from "./lib/utils";

export interface UploadProgressProps extends React.HTMLAttributes<HTMLDivElement> {
  filename: string;
  /** 0-100 */
  progress: number;
  onCancel?: () => void;
}

/** Single file upload progress: circular gauge + filename + cancel. */
export function UploadProgress({ filename, progress, onCancel, className, ...props }: UploadProgressProps) {
  const R = 16;
  const C = 2 * Math.PI * R;
  const done = progress >= 100;

  return (
    <div className={cn("flex w-full max-w-sm items-center gap-3 rounded-md border border-border bg-card px-3 py-2.5", className)} {...props}>
      <style>{`@keyframes upFade { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }`}</style>
      <span className="relative size-10 shrink-0" role="progressbar" aria-valuenow={progress} aria-valuemin={0} aria-valuemax={100} aria-label={filename}>
        <svg viewBox="0 0 40 40" className="size-full -rotate-90">
          <circle cx="20" cy="20" r={R} fill="none" stroke="var(--border)" strokeWidth="3" />
          <circle
            cx="20" cy="20" r={R} fill="none"
            stroke="currentColor"
            strokeWidth="3"
            strokeLinecap="round"
            className={done ? "text-success" : "text-accent"}
            strokeDasharray={C}
            strokeDashoffset={C - (C * Math.min(100, progress)) / 100}
            style={{ transition: "stroke-dashoffset 0.4s ease-out" }}
          />
        </svg>
        <span className="absolute inset-0 flex items-center justify-center font-mono text-[9px] font-bold tabular-nums">
          {done ? "✓" : `${progress}%`}
        </span>
      </span>
      <div className="min-w-0 flex-1 animate-[upFade_0.3s_ease-out_both] motion-reduce:animate-none">
        <p className="truncate text-sm font-medium">{filename}</p>
        <p className="text-xs text-muted-foreground">{done ? "Upload complete" : "Uploading…"}</p>
      </div>
      {!done && (
        <button
          onClick={onCancel}
          className="shrink-0 rounded-sm p-1 text-muted-foreground transition-colors hover:text-destructive focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring motion-reduce:transition-none"
          aria-label={`Cancel ${filename} upload`}
        >
          <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true"><path d="M18 6L6 18M6 6l12 12" /></svg>
        </button>
      )}
    </div>
  );
}

Props

PropTypeDescription
filenamestring
progressnumber0-100
onCancel() => void