
RIFT
Februar 2025
RIFT is a file transfer tool—in the same category as WeTransfer or Dropbox. However, it's not another clean, forgettable drag-and-drop box. Each demo below is the live component, straight from RIFT's own Storybook, not a screen recording.

What It Is
You open a 'RIFT'—the label on the creation card reads 'RIFT CHANNEL / Open New RIFT.' Drop files, get a link. The free tier defaults a transfer to 2 GB with a 3-day validity; paid tiers increase both. Each file in a transfer is counted as a 'Shard' in the UI ('2 Shards · 1 GB')—this is the only gaming term that made it into the shipped product.
Large uploads are chunked and reassembled server-side against Supabase Storage, with Supabase also managing authentication and the transfer database.Next.js 16 on Turbopack powers the app, Base UI provides the unstyled primitives,Tailwind handles the styling, Framer Motion controls transitions.
Halftone Backdrop and Image Dithering
The dithering-like background isn't a CSS gradient trick—it's a GPU shader. RiftHalftoneBackdrop renders a HalftoneDots-shader from @paper-design/shaders-react over a background image and observes prefers-color-scheme, so it adapts in dark mode. The same library enables image dithering elsewhere in the UI.
Both uses exist for the same reason: @paper-design/shaders-react initializes its canvas to a hardcoded 300×150 pixels by default. Each wrapper adds a ResizeObserver to correct the canvas to its actual container size on mount and resize.
// Recurring fix, used by both RiftHalftoneBackdrop and ImageDitheringWrapper
useEffect(() => {
const container = containerRef.current
if (!container) return
const resizeObserver = new ResizeObserver((entries) => {
const canvas = container.querySelector('canvas')
if (!canvas) return
const { width, height } = entries[0].contentRect
canvas.width = width
canvas.height = height
})
resizeObserver.observe(container)
return () => resizeObserver.disconnect()
}, [])Upload: Three Steps, a PIN-like Password
UploadCard uses a simple three-step state machine—Upload, Configure, Uploading—instead of a wizard library. The configuration step sets the validity period (defaulting to 3 days for free accounts), an optional download limit, and an optional 6-digit numeric password entered like a phone PIN: one field per digit, with auto-advancing focus.
type Step = "upload" | "config" | "uploading"
const [step, setStep] = useState<Step>("upload")
const [expirationDays, setExpirationDays] = useState(3) // free-tier default
const [passwordChars, setPasswordChars] = useState<string[]>(Array(6).fill(""))
const [enablePassword, setEnablePassword] = useState(false)
const activeStep: Step = isUploading ? "uploading" : stepWhat the Recipient Sees
DownloadPage protects access with the same 6-digit code if a transfer is password-protected. Before the code is entered, the sender's email is run through EmailScramble—a deterministic hash encrypts it into placeholder characters, then decrypts it character by character once revealed, rather than simply toggling visibility with CSS.
// Deterministic hash → placeholder chars, so the scramble is
// stable per email instead of re-randomizing on every render
function makePlaceholder(seed: string, length: number): string {
let h = 0
for (let i = 0; i < seed.length; i++) h = ((h << 5) - h + seed.charCodeAt(i)) | 0
let out = ""
for (let i = 0; i < length; i++) {
h = Math.imul(1103515245, h) + 12345
out += CHARS[Math.abs(h >>> 0) % CHARS.length]
}
return out
}
// revealed flips true only after password verification succeeds
<EmailScramble text={senderEmail} revealed={!needsPassword || isVerified} />Warnings with Hazard Stripes
Warnings and errors use a hazard mode—literally diagonal hazard stripe backgrounds that sit beneath the status color, more like industrial tape than a typical toast window.
The Drawer Stack
Clicking 'History' opens a history drawer. Clicking a transfer within it opens a nested detail drawer on top. A file preview from there opens a media preview modal on top of that—three stacked overlays that required real iteration to implement correctly without one swallowing the others' clicks.

The drawers started with Vaul, then migrated to Base UI's Dialog with modal={true}—scroll locking is included for free, and enter/exit animations are based on data-[open]/data-[closed] attributes instead of a swipe gesture library.
Sidebar z-40
TransferModal overlay z-50
TransferModal content z-[60]
TransferDetailDrawer backdrop z-[65]
TransferDetailDrawer popup z-[70]
Sonner toasts z-99998The nesting follows a rule that took a debugging session to learn: the detail drawer is nested as a true child dialog (keepMounted so exit animations can complete) within the history drawer, but the media preview modal is lifted to the top level—a sibling of both drawers, not a child. An AlertDialog or Modal nested within another dialog closes both on dismissal, so anything that can outlive its parent's lifecycle is rendered as a sibling instead.
// TransferModal owns preview state, both drawers just report into it
const [preview, setPreview] = useState<{ files: Shard[]; index: number } | null>(null)
<Dialog.Root open={verlaufOpen}>
{/* ...transfer list... */}
<TransferDetailDrawer
onOpenPreview={(files, index) => setPreview({ files, index })}
/>
</Dialog.Root>
{/* sibling, not nested — survives the drawer above it closing */}
<MediaPreviewModal state={preview} onClose={() => setPreview(null)} />Snap Scroll and a Ref Instead of State
The homepage snap-scrolls between sections on mouse input. Opening a drawer or modal must interrupt this, or scrolling the drawer's content will also scroll the page beneath. The naive solution is a state value that the wheel handler checks on every event, which means re-registering the listener every time a modal opens or closes.
// MainLayout — a ref instead of state, so the listener registers once
const modalOpenRef = useRef(false)
useEffect(() => {
function handleWheel(e: WheelEvent) {
if (modalOpenRef.current) return // suspended, no re-register needed
// ...snap-scroll logic
}
window.addEventListener("wheel", handleWheel, { passive: false })
return () => window.removeEventListener("wheel", handleWheel)
}, []) // empty deps — the ref makes that safe
// opening any modal just flips the ref, no re-render required
const openModal = () => { modalOpenRef.current = true; setModalOpen(true) }Stack
- Next.js 16 (Turbopack)
- Supabase – Authentication, Database, segmented file storage
- Base UI (Dialog/AlertDialog primitives) + Tailwind
- Framer Motion for transitions
- @paper-design/shaders-react for halftone backdrop and image dithering
- German-first UI, with 'Shard'/'RIFT' as the remaining gaming terms