diff --git a/apps/remix/app/components/dialogs/envelopes-bulk-download-dialog.tsx b/apps/remix/app/components/dialogs/envelopes-bulk-download-dialog.tsx
new file mode 100644
index 000000000..940055854
--- /dev/null
+++ b/apps/remix/app/components/dialogs/envelopes-bulk-download-dialog.tsx
@@ -0,0 +1,377 @@
+import {
+ createZipWriter,
+ sanitizeZipPathSegment,
+ type ZipFileEntry,
+} from '@documenso/lib/client-only/create-zip-writer';
+import { downloadFile } from '@documenso/lib/client-only/download-file';
+import { fetchPDF } from '@documenso/lib/client-only/download-pdf';
+import { trpc } from '@documenso/trpc/react';
+import { Alert, AlertDescription } from '@documenso/ui/primitives/alert';
+import { Button } from '@documenso/ui/primitives/button';
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from '@documenso/ui/primitives/dialog';
+import { RadioGroupSegmented, RadioGroupSegmentedItem } from '@documenso/ui/primitives/radio-group';
+import { useToast } from '@documenso/ui/primitives/use-toast';
+import { plural } from '@lingui/core/macro';
+import { Plural, Trans, useLingui } from '@lingui/react/macro';
+import { DocumentStatus } from '@prisma/client';
+import type * as DialogPrimitive from '@radix-ui/react-dialog';
+import { useEffect, useRef, useState } from 'react';
+import { match } from 'ts-pattern';
+
+/**
+ * The maximum number of documents that can be downloaded in a single bulk
+ * download. Each document requires fetching its full PDFs into the browser,
+ * so this bounds both request volume and blob storage usage. Matches the
+ * spirit of the server-side 100 cap on bulk move/delete/cancel.
+ */
+export const MAX_BULK_DOWNLOAD_ENVELOPES = 50;
+
+type BulkDownloadVersion = 'signed' | 'original' | 'pending';
+
+export type EnvelopeBulkDownloadItem = {
+ id: string;
+ title: string;
+ status: DocumentStatus;
+
+ /**
+ * Whether the envelope is a legacy (v1) envelope. Legacy envelopes use a
+ * different field-rendering pipeline that the partial PDF helper does not
+ * implement, so the Partial option is hidden for them.
+ */
+ isLegacy: boolean;
+};
+
+const getDefaultVersion = (envelope: EnvelopeBulkDownloadItem): BulkDownloadVersion =>
+ envelope.status === DocumentStatus.COMPLETED ? 'signed' : 'original';
+
+export type EnvelopesBulkDownloadDialogProps = {
+ envelopes: EnvelopeBulkDownloadItem[];
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ onSuccess?: (successfulEnvelopeIds: string[]) => void;
+} & Omit