A file-sharing card where each hint rubs itself out once you've done what it asks, and the next one writes in.
registry/demos/jot-onboarding-demo.tsx
"use client";
import * as React from "react";
import { Jot } from "@/components/ui/jot";
const FILE = { name: "Brand guidelines.pdf", size: "4.2 MB" };
const LINK = "parcel.to/k7f2q";
type Step = "empty" | "uploading" | "ready" | "sent";
export function JotOnboardingDemo() {
const [step, setStep] = React.useState<Step>("empty");
const dropRef = React.useRef<HTMLButtonElement>(null);
const copyRef = React.useRef<HTMLButtonElement>(null);
// A short, pretend upload, so the second hint waits for the file to land.
React.useEffect(() => {
if (step !== "uploading") return;
const timer = window.setTimeout(() => setStep("ready"), 900);
return () => window.clearTimeout(timer);
}, [step]);
const hasFile = step !== "empty";
const sent = step === "sent";
return (
<div
data-jot-scope
className="@container/share relative grid w-full max-w-[52rem] justify-items-center px-2 pt-20 pb-24 @2xl/share:px-0 @2xl/share:py-10"
>
<div className="w-full max-w-[22rem] rounded-[1.5rem] border border-zinc-950/10 bg-white p-5 shadow-[0_1px_2px_rgb(0_0_0/0.04),0_12px_32px_-12px_rgb(0_0_0/0.12)] dark:border-white/10 dark:bg-zinc-900 dark:shadow-none">
<div className="flex items-center justify-between text-xs text-zinc-500 dark:text-zinc-400">
<span className="flex items-center gap-1.5 font-medium text-zinc-950 dark:text-white">
<svg viewBox="0 0 16 16" aria-hidden className="size-4">
<path d="M2.5 5.2 8 2.5l5.5 2.7v5.6L8 13.5l-5.5-2.7z" className="fill-amber-400" />
<path d="M2.5 5.2 8 8l5.5-2.8M8 8v5.5" className="fill-none stroke-amber-700" strokeWidth="1.1" strokeLinejoin="round" />
</svg>
Parcel
</span>
Links last 7 days
</div>
<p className="mt-4 text-lg/tight font-semibold tracking-[-0.02em] text-zinc-950 dark:text-white">Send files</p>
<button
ref={dropRef}
type="button"
disabled={hasFile}
onClick={() => setStep("uploading")}
className="mt-3 grid w-full justify-items-center gap-1 rounded-2xl border border-dashed border-zinc-950/20 bg-zinc-50 px-4 py-6 text-center outline-none transition-colors duration-150 enabled:hover:border-zinc-950/40 enabled:hover:bg-zinc-100/70 focus-visible:ring-2 focus-visible:ring-blue-500 disabled:cursor-default disabled:opacity-50 dark:border-white/15 dark:bg-white/[0.03] dark:enabled:hover:border-white/30 dark:enabled:hover:bg-white/[0.06]"
>
<svg viewBox="0 0 20 20" aria-hidden className="size-5 text-zinc-500 dark:text-zinc-400">
<path d="M10 13V4m0 0L6.5 7.5M10 4l3.5 3.5M4 13.5v1A1.5 1.5 0 0 0 5.5 16h9a1.5 1.5 0 0 0 1.5-1.5v-1" className="fill-none stroke-current" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
<span className="text-sm font-medium text-zinc-950 dark:text-white">Drop files here</span>
<span className="text-xs text-zinc-500 dark:text-zinc-400">or click to add a sample file</span>
</button>
<div className="mt-3 flex h-12 items-center gap-3 rounded-xl px-1" aria-live="polite">
{hasFile ? (
<>
<span className="grid size-9 shrink-0 place-items-center rounded-lg bg-red-50 text-[10px] font-semibold text-red-600 dark:bg-red-500/10 dark:text-red-400">
PDF
</span>
<span className="min-w-0 flex-1">
<span className="block truncate text-sm font-medium text-zinc-950 dark:text-white">{FILE.name}</span>
<span className="mt-1 block h-1 overflow-hidden rounded-full bg-zinc-100 dark:bg-white/10">
<span
className={
step === "uploading"
? "block h-full w-full origin-left rounded-full bg-zinc-950 transition-transform duration-[900ms] ease-out starting:scale-x-0 dark:bg-white"
: "block h-full w-full rounded-full bg-emerald-500"
}
/>
</span>
</span>
<span className="text-xs text-zinc-500 tabular-nums dark:text-zinc-400">
{step === "uploading" ? "Uploading" : FILE.size}
</span>
</>
) : (
<span className="w-full text-center text-xs text-zinc-400 dark:text-zinc-500">No files yet</span>
)}
</div>
<div className="mt-3 flex items-center gap-2 border-t border-zinc-950/[0.06] pt-4 dark:border-white/[0.06]">
<span className="min-w-0 flex-1 truncate rounded-full bg-zinc-100 px-3 py-2 font-mono text-xs text-zinc-500 dark:bg-white/5 dark:text-zinc-400">
{step === "ready" || sent ? LINK : "Your link shows up here"}
</span>
<button
ref={copyRef}
type="button"
disabled={step !== "ready" && !sent}
onClick={() => setStep("sent")}
className="inline-flex h-9 shrink-0 items-center gap-1.5 rounded-full bg-zinc-950 px-4 text-sm font-medium text-white outline-none transition-opacity duration-150 enabled:hover:bg-zinc-800 focus-visible:ring-2 focus-visible:ring-zinc-950 focus-visible:ring-offset-2 disabled:opacity-35 dark:bg-white dark:text-zinc-950 dark:enabled:hover:bg-zinc-200 dark:focus-visible:ring-white dark:focus-visible:ring-offset-zinc-900"
>
{sent ? (
<>
<svg viewBox="0 0 16 16" aria-hidden className="size-4">
<path d="m3.5 8.5 3 3 6-7" className="fill-none stroke-current" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round" />
</svg>
Copied
</>
) : (
"Copy link"
)}
</button>
</div>
</div>
<button
type="button"
onClick={() => setStep("empty")}
className={
"mt-4 rounded-sm text-xs font-medium text-zinc-500 underline decoration-zinc-300 underline-offset-4 outline-none transition-opacity duration-200 hover:text-zinc-950 focus-visible:ring-2 focus-visible:ring-zinc-950 dark:text-zinc-400 dark:decoration-zinc-700 dark:hover:text-white " +
(sent ? "opacity-100" : "pointer-events-none opacity-0")
}
tabIndex={sent ? 0 : -1}
aria-hidden={!sent}
>
Start over
</button>
<Jot
to={dropRef}
show={step === "empty"}
bend={0.3}
tilt={-3}
className="absolute top-4 left-4 text-xl/snug text-blue-600 @2xl/share:top-[30%] @2xl/share:left-4 dark:text-blue-400"
>
start here: drop
<br />
anything in
</Jot>
<Jot
to={copyRef}
show={step === "ready"}
bend={0.35}
tilt={2}
className="absolute right-4 bottom-6 text-xl/snug text-blue-600 @2xl/share:top-[62%] @2xl/share:right-4 @2xl/share:bottom-auto dark:text-blue-400"
>
now grab the link
</Jot>
<Jot
show={sent}
tilt={-2}
className="absolute bottom-6 left-4 text-xl/snug text-emerald-600 @2xl/share:top-[64%] @2xl/share:bottom-auto @2xl/share:left-4 dark:text-emerald-400"
>
that's the whole thing
</Jot>
</div>
);
}
Live preview, examples and props: https://keyframeui.com/components/jot#jot-onboarding-demo