Desktop 4: licence activation
Licence activation for a paid desktop app: a trial meter, a key field that cleans up pasted keys, activation that shows why a key failed, the licensed view with deactivate, and a trial-ended mode.
The normal view. Pick a state to see what the block shows while it loads, is empty or fails.
// Ballmac UI: Desktop 4. https://ui.ballmac.com/blocks/desktop-4
"use client"
import * as React from "react"
import { BadgeCheck, CircleAlert, KeyRound, Loader2, Sparkles } from "lucide-react"
import { AppWindow, AppWindowContent, AppWindowTitleBar } from "@/components/ballmac/app-window"
import { Button, buttonVariants } from "@/components/ballmac/button"
import { Input } from "@/components/ballmac/input"
import { Progress } from "@/components/ballmac/progress"
import { frameProps } from "@/components/ballmac/state-view"
import { useFeel, type FeelId } from "@/lib/ballmac/feel"
import { formPhase, type FormStateProps } from "@/lib/ballmac/use-resource"
import { cn } from "@/lib/utils"
type Desktop4Licence = {
/** Who the licence belongs to, usually the buyer's email. */
licensedTo: string
/** The plan name, for example "Pro". */
plan?: string
}
type Desktop4Props = Omit<React.ComponentProps<"div">, "children" | "onSubmit"> &
FormStateProps & {
/** Which platform the window handles like. Defaults to the page's feel. */
feel?: FeelId
/** Draw the window frame. Turn off inside a real Tauri or Electron window that already has one. */
frame?: boolean
appName?: string
/** The app icon. A generated one is drawn when you leave it out. */
icon?: React.ReactNode
/** Days left in the trial. 0 means the trial has ended. */
trialDaysLeft?: number
/** The length of the trial in days, for the meter. */
trialLength?: number
/** Where "Buy a licence" goes. */
buyUrl?: string
/** One line about seats, for example "One licence works on up to 3 Macs." */
seatsNote?: string
/** An active licence. Pass it and the window opens on the licensed view. */
licence?: Desktop4Licence
/**
* Called with the trimmed key. Resolve with the licence to show who it belongs to; throw an Error to show its message under the
* field. Defaults to a simulated check that accepts any key of 16 or more characters.
*/
onActivate?: (key: string) => Desktop4Licence | void | Promise<Desktop4Licence | void>
/** Called by "Continue trial". Not shown once the trial has ended. */
onContinueTrial?: () => void
/** Called by "Quit" once the trial has ended. */
onQuit?: () => void
/** Called by "Deactivate" in the licensed view, to free the seat. */
onDeactivate?: () => void | Promise<void>
/** Called by Done in the licensed view, and by the close button. */
onDone?: () => void
}
function AppIcon({ icon, className }: { icon?: React.ReactNode; className?: string }) {
return (
<span
aria-hidden="true"
className={cn(
"from-primary to-primary/70 text-primary-foreground flex shrink-0 items-center justify-center rounded-[22%] bg-linear-to-b shadow-[0_1px_2px_rgb(0_0_0/0.2),inset_0_1px_0_rgb(255_255_255/0.25)] [&_svg]:size-1/2",
className
)}
>
{icon ?? <Sparkles />}
</span>
)
}
function Actions({ feel, start, children }: { feel: FeelId; start?: React.ReactNode; children: React.ReactNode[] }) {
const ordered = feel === "windows" ? [...children].reverse() : children
return (
<div data-slot="desktop-4-actions" className="flex flex-wrap items-center gap-2">
{start}
<div className="ms-auto flex flex-wrap justify-end gap-2">{ordered}</div>
</div>
)
}
/**
* Licence activation for a paid desktop app: the trial meter, a key field that accepts a pasted key with spaces or line breaks,
* activation with the reason when it fails, and the licensed view with deactivate. Wording follows the platform ("this Mac",
* "this PC") and its button order.
*/
function Desktop4({
feel: feelProp,
frame = true,
appName = "Acme",
icon,
trialDaysLeft = 9,
trialLength = 14,
buyUrl = "#",
seatsNote,
licence: licenceProp,
onActivate,
onContinueTrial,
onQuit,
onDeactivate,
onDone,
state: stateProp,
errorState,
className,
...props
}: Desktop4Props) {
const page = useFeel().feel
const feel = feelProp ?? page
const win = feel === "windows"
const mac = feel === "macos"
const device = mac ? "this Mac" : win ? "this PC" : "this device"
const id = React.useId()
const [key, setKey] = React.useState("")
const [licence, setLicence] = React.useState<Desktop4Licence | undefined>(licenceProp)
React.useEffect(() => setLicence(licenceProp), [licenceProp])
const [phase, setPhase] = React.useState<"idle" | "sending" | "error" | "done">(licenceProp ? "done" : "idle")
const [message, setMessage] = React.useState<string>()
const forced = formPhase(stateProp)
const current = forced ?? phase
const expired = trialDaysLeft <= 0
const t = (macText: string, other: string) => (mac ? macText : other)
async function activate(e?: React.FormEvent) {
e?.preventDefault()
const clean = key.replace(/\s+/g, "")
if (!clean) {
setPhase("error")
setMessage("Enter the licence key from your receipt email.")
return
}
setPhase("sending")
setMessage(undefined)
try {
let result: Desktop4Licence | void
if (onActivate) result = await onActivate(clean)
else {
await new Promise((r) => setTimeout(r, 900))
if (clean.length < 16) throw new Error("That key is not valid. Check that you copied all of it.")
result = { licensedTo: "you@example.com", plan: "Pro" }
}
setLicence(result || { licensedTo: "", plan: undefined })
setPhase("done")
} catch (err) {
setPhase("error")
setMessage(err instanceof Error && err.message ? err.message : "The key could not be checked. Try again in a moment.")
}
}
const done = current === "done"
const sending = current === "sending"
const errorText = current === "error" ? (forced === "error" ? (errorState?.description ?? "This key has reached its device limit. Deactivate it on another device, then try again.") : message) : undefined
const content = done ? (
<div className="flex flex-1 flex-col gap-5 p-6">
<div className={cn("flex gap-4", mac ? "flex-col items-center text-center" : "items-center")}>
<AppIcon icon={icon} className={mac ? "size-16" : "size-12"} />
<div aria-live="polite">
<h2 id={`${id}-title`} className="flex items-center gap-2 text-lg font-semibold">
<BadgeCheck aria-hidden="true" className="text-chart-2 size-5" />
{appName} {licence?.plan ?? "Pro"} is activated
</h2>
<p className="text-muted-foreground mt-1 text-[13px]">
{licence?.licensedTo ? <>Licensed to <span className="text-foreground font-medium break-all">{licence.licensedTo}</span> on {device}.</> : <>Activated on {device}. Thank you for your support.</>}
</p>
</div>
</div>
<div className="flex-1" />
<Actions
feel={feel}
start={
onDeactivate ? (
<Button variant="link" size="sm" className="px-0" onClick={async () => { await onDeactivate(); setLicence(undefined); setPhase("idle"); setKey("") }}>
{t("Deactivate This Mac", `Deactivate ${device}`)}
</Button>
) : null
}
>
{[
<Button key="done" size="sm" onClick={onDone}>
Done
</Button>,
]}
</Actions>
</div>
) : (
<form onSubmit={activate} noValidate className="flex flex-1 flex-col gap-5 p-6">
<div className={cn("flex gap-4", mac ? "flex-col items-center text-center" : "items-start")}>
<AppIcon icon={icon} className={mac ? "size-16" : "size-12"} />
<div className="min-w-0">
<h2 id={`${id}-title`} className="text-lg font-semibold text-pretty">
{expired ? "Your free trial has ended" : `Activate ${appName}`}
</h2>
<p className="text-muted-foreground mt-1 text-[13px] text-pretty">
{expired
? `Enter a licence key to keep using ${appName}. Your data is safe and waiting.`
: `Enter the licence key from your receipt to unlock ${appName} on ${device}.`}
</p>
</div>
</div>
{!expired ? (
<div className="grid gap-1.5">
<div className="flex justify-between text-xs">
<span className="text-muted-foreground">Free trial</span>
<span className="font-medium tabular-nums">{trialDaysLeft === 1 ? "1 day left" : `${trialDaysLeft} days left`}</span>
</div>
<Progress value={(trialDaysLeft / trialLength) * 100} aria-label={`Free trial: ${trialDaysLeft} of ${trialLength} days left`} />
</div>
) : null}
<div className="grid gap-1.5">
<label htmlFor={`${id}-key`} className="text-sm font-medium">
Licence key
</label>
<Input
id={`${id}-key`}
dir="ltr"
value={key}
onChange={(e) => {
setKey(e.target.value)
if (phase === "error") setPhase("idle")
}}
placeholder="XXXX-XXXX-XXXX-XXXX"
autoComplete="off"
autoCapitalize="characters"
spellCheck={false}
readOnly={sending}
aria-invalid={!!errorText || undefined}
aria-describedby={errorText ? `${id}-error` : seatsNote ? `${id}-seats` : undefined}
className="font-mono tracking-wider"
/>
{errorText ? (
<p id={`${id}-error`} role="alert" className="text-destructive flex items-start gap-1.5 text-[13px]">
<CircleAlert aria-hidden="true" className="mt-0.5 size-3.5 shrink-0" />
{errorText}
</p>
) : seatsNote ? (
<p id={`${id}-seats`} className="text-muted-foreground text-xs">
{seatsNote}
</p>
) : null}
</div>
<div className="flex-1" />
<Actions
feel={feel}
start={
<a href={buyUrl} target="_blank" rel="noreferrer" className={cn(buttonVariants({ variant: "link", size: "sm" }), "px-0")}>
{t("Buy a Licence…", "Buy a licence")}
</a>
}
>
{[
expired ? (
<Button key="quit" type="button" variant="outline" size="sm" onClick={onQuit}>
{t(`Quit ${appName}`, "Quit")}
</Button>
) : (
<Button key="trial" type="button" variant="outline" size="sm" onClick={onContinueTrial} disabled={sending}>
{t("Continue Trial", "Continue trial")}
</Button>
),
<Button key="activate" type="submit" size="sm" disabled={sending} aria-disabled={sending || undefined}>
{sending ? <Loader2 aria-hidden="true" className="animate-spin motion-reduce:animate-none" /> : <KeyRound aria-hidden="true" />}
{sending ? "Activating…" : "Activate"}
</Button>,
]}
</Actions>
<p aria-live="polite" className="sr-only">
{sending ? "Checking the licence key" : ""}
</p>
</form>
)
if (!frame)
return (
<div data-slot="desktop-4" data-state={done ? "success" : current === "sending" ? "submitting" : current === "error" ? "error" : "ready"} data-feel={feelProp} role="region" aria-labelledby={`${id}-title`} className={cn("flex flex-col", className)} {...frameProps(props)}>
{content}
</div>
)
return (
<AppWindow
data-slot="desktop-4"
data-state={done ? "success" : current === "sending" ? "submitting" : current === "error" ? "error" : "ready"}
feel={feel}
onClose={onDone}
role="region"
aria-labelledby={`${id}-title`}
className={cn("h-[30rem] w-full max-w-md", className)}
{...frameProps(props)}
>
<AppWindowTitleBar title={win ? `${appName} licence` : ""} icon={<KeyRound />} className="border-b-0" />
<AppWindowContent className="flex flex-col">{content}</AppWindowContent>
</AppWindow>
)
}
export { Desktop4, type Desktop4Props, type Desktop4Licence }Installation
$ pnpm dlx shadcn@latest add @ballmac/desktop-4Install the dependencies.
$ pnpm add lucide-reactAdd the Ballmac items it builds on.
$ pnpm dlx shadcn@latest add @ballmac/app-window @ballmac/button @ballmac/input @ballmac/progress @ballmac/state-view @ballmac/use-resource @ballmac/feel-presetsCopy the source into your project.
components/ballmac/blocks/desktop-4/desktop-4.tsx// Ballmac UI: Desktop 4. https://ui.ballmac.com/blocks/desktop-4 "use client" import * as React from "react" import { BadgeCheck, CircleAlert, KeyRound, Loader2, Sparkles } from "lucide-react" import { AppWindow, AppWindowContent, AppWindowTitleBar } from "@/components/ballmac/app-window" import { Button, buttonVariants } from "@/components/ballmac/button" import { Input } from "@/components/ballmac/input" import { Progress } from "@/components/ballmac/progress" import { frameProps } from "@/components/ballmac/state-view" import { useFeel, type FeelId } from "@/lib/ballmac/feel" import { formPhase, type FormStateProps } from "@/lib/ballmac/use-resource" import { cn } from "@/lib/utils" type Desktop4Licence = { /** Who the licence belongs to, usually the buyer's email. */ licensedTo: string /** The plan name, for example "Pro". */ plan?: string } type Desktop4Props = Omit<React.ComponentProps<"div">, "children" | "onSubmit"> & FormStateProps & { /** Which platform the window handles like. Defaults to the page's feel. */ feel?: FeelId /** Draw the window frame. Turn off inside a real Tauri or Electron window that already has one. */ frame?: boolean appName?: string /** The app icon. A generated one is drawn when you leave it out. */ icon?: React.ReactNode /** Days left in the trial. 0 means the trial has ended. */ trialDaysLeft?: number /** The length of the trial in days, for the meter. */ trialLength?: number /** Where "Buy a licence" goes. */ buyUrl?: string /** One line about seats, for example "One licence works on up to 3 Macs." */ seatsNote?: string /** An active licence. Pass it and the window opens on the licensed view. */ licence?: Desktop4Licence /** * Called with the trimmed key. Resolve with the licence to show who it belongs to; throw an Error to show its message under the * field. Defaults to a simulated check that accepts any key of 16 or more characters. */ onActivate?: (key: string) => Desktop4Licence | void | Promise<Desktop4Licence | void> /** Called by "Continue trial". Not shown once the trial has ended. */ onContinueTrial?: () => void /** Called by "Quit" once the trial has ended. */ onQuit?: () => void /** Called by "Deactivate" in the licensed view, to free the seat. */ onDeactivate?: () => void | Promise<void> /** Called by Done in the licensed view, and by the close button. */ onDone?: () => void } function AppIcon({ icon, className }: { icon?: React.ReactNode; className?: string }) { return ( <span aria-hidden="true" className={cn( "from-primary to-primary/70 text-primary-foreground flex shrink-0 items-center justify-center rounded-[22%] bg-linear-to-b shadow-[0_1px_2px_rgb(0_0_0/0.2),inset_0_1px_0_rgb(255_255_255/0.25)] [&_svg]:size-1/2", className )} > {icon ?? <Sparkles />} </span> ) } function Actions({ feel, start, children }: { feel: FeelId; start?: React.ReactNode; children: React.ReactNode[] }) { const ordered = feel === "windows" ? [...children].reverse() : children return ( <div data-slot="desktop-4-actions" className="flex flex-wrap items-center gap-2"> {start} <div className="ms-auto flex flex-wrap justify-end gap-2">{ordered}</div> </div> ) } /** * Licence activation for a paid desktop app: the trial meter, a key field that accepts a pasted key with spaces or line breaks, * activation with the reason when it fails, and the licensed view with deactivate. Wording follows the platform ("this Mac", * "this PC") and its button order. */ function Desktop4({ feel: feelProp, frame = true, appName = "Acme", icon, trialDaysLeft = 9, trialLength = 14, buyUrl = "#", seatsNote, licence: licenceProp, onActivate, onContinueTrial, onQuit, onDeactivate, onDone, state: stateProp, errorState, className, ...props }: Desktop4Props) { const page = useFeel().feel const feel = feelProp ?? page const win = feel === "windows" const mac = feel === "macos" const device = mac ? "this Mac" : win ? "this PC" : "this device" const id = React.useId() const [key, setKey] = React.useState("") const [licence, setLicence] = React.useState<Desktop4Licence | undefined>(licenceProp) React.useEffect(() => setLicence(licenceProp), [licenceProp]) const [phase, setPhase] = React.useState<"idle" | "sending" | "error" | "done">(licenceProp ? "done" : "idle") const [message, setMessage] = React.useState<string>() const forced = formPhase(stateProp) const current = forced ?? phase const expired = trialDaysLeft <= 0 const t = (macText: string, other: string) => (mac ? macText : other) async function activate(e?: React.FormEvent) { e?.preventDefault() const clean = key.replace(/\s+/g, "") if (!clean) { setPhase("error") setMessage("Enter the licence key from your receipt email.") return } setPhase("sending") setMessage(undefined) try { let result: Desktop4Licence | void if (onActivate) result = await onActivate(clean) else { await new Promise((r) => setTimeout(r, 900)) if (clean.length < 16) throw new Error("That key is not valid. Check that you copied all of it.") result = { licensedTo: "you@example.com", plan: "Pro" } } setLicence(result || { licensedTo: "", plan: undefined }) setPhase("done") } catch (err) { setPhase("error") setMessage(err instanceof Error && err.message ? err.message : "The key could not be checked. Try again in a moment.") } } const done = current === "done" const sending = current === "sending" const errorText = current === "error" ? (forced === "error" ? (errorState?.description ?? "This key has reached its device limit. Deactivate it on another device, then try again.") : message) : undefined const content = done ? ( <div className="flex flex-1 flex-col gap-5 p-6"> <div className={cn("flex gap-4", mac ? "flex-col items-center text-center" : "items-center")}> <AppIcon icon={icon} className={mac ? "size-16" : "size-12"} /> <div aria-live="polite"> <h2 id={`${id}-title`} className="flex items-center gap-2 text-lg font-semibold"> <BadgeCheck aria-hidden="true" className="text-chart-2 size-5" /> {appName} {licence?.plan ?? "Pro"} is activated </h2> <p className="text-muted-foreground mt-1 text-[13px]"> {licence?.licensedTo ? <>Licensed to <span className="text-foreground font-medium break-all">{licence.licensedTo}</span> on {device}.</> : <>Activated on {device}. Thank you for your support.</>} </p> </div> </div> <div className="flex-1" /> <Actions feel={feel} start={ onDeactivate ? ( <Button variant="link" size="sm" className="px-0" onClick={async () => { await onDeactivate(); setLicence(undefined); setPhase("idle"); setKey("") }}> {t("Deactivate This Mac", `Deactivate ${device}`)} </Button> ) : null } > {[ <Button key="done" size="sm" onClick={onDone}> Done </Button>, ]} </Actions> </div> ) : ( <form onSubmit={activate} noValidate className="flex flex-1 flex-col gap-5 p-6"> <div className={cn("flex gap-4", mac ? "flex-col items-center text-center" : "items-start")}> <AppIcon icon={icon} className={mac ? "size-16" : "size-12"} /> <div className="min-w-0"> <h2 id={`${id}-title`} className="text-lg font-semibold text-pretty"> {expired ? "Your free trial has ended" : `Activate ${appName}`} </h2> <p className="text-muted-foreground mt-1 text-[13px] text-pretty"> {expired ? `Enter a licence key to keep using ${appName}. Your data is safe and waiting.` : `Enter the licence key from your receipt to unlock ${appName} on ${device}.`} </p> </div> </div> {!expired ? ( <div className="grid gap-1.5"> <div className="flex justify-between text-xs"> <span className="text-muted-foreground">Free trial</span> <span className="font-medium tabular-nums">{trialDaysLeft === 1 ? "1 day left" : `${trialDaysLeft} days left`}</span> </div> <Progress value={(trialDaysLeft / trialLength) * 100} aria-label={`Free trial: ${trialDaysLeft} of ${trialLength} days left`} /> </div> ) : null} <div className="grid gap-1.5"> <label htmlFor={`${id}-key`} className="text-sm font-medium"> Licence key </label> <Input id={`${id}-key`} dir="ltr" value={key} onChange={(e) => { setKey(e.target.value) if (phase === "error") setPhase("idle") }} placeholder="XXXX-XXXX-XXXX-XXXX" autoComplete="off" autoCapitalize="characters" spellCheck={false} readOnly={sending} aria-invalid={!!errorText || undefined} aria-describedby={errorText ? `${id}-error` : seatsNote ? `${id}-seats` : undefined} className="font-mono tracking-wider" /> {errorText ? ( <p id={`${id}-error`} role="alert" className="text-destructive flex items-start gap-1.5 text-[13px]"> <CircleAlert aria-hidden="true" className="mt-0.5 size-3.5 shrink-0" /> {errorText} </p> ) : seatsNote ? ( <p id={`${id}-seats`} className="text-muted-foreground text-xs"> {seatsNote} </p> ) : null} </div> <div className="flex-1" /> <Actions feel={feel} start={ <a href={buyUrl} target="_blank" rel="noreferrer" className={cn(buttonVariants({ variant: "link", size: "sm" }), "px-0")}> {t("Buy a Licence…", "Buy a licence")} </a> } > {[ expired ? ( <Button key="quit" type="button" variant="outline" size="sm" onClick={onQuit}> {t(`Quit ${appName}`, "Quit")} </Button> ) : ( <Button key="trial" type="button" variant="outline" size="sm" onClick={onContinueTrial} disabled={sending}> {t("Continue Trial", "Continue trial")} </Button> ), <Button key="activate" type="submit" size="sm" disabled={sending} aria-disabled={sending || undefined}> {sending ? <Loader2 aria-hidden="true" className="animate-spin motion-reduce:animate-none" /> : <KeyRound aria-hidden="true" />} {sending ? "Activating…" : "Activate"} </Button>, ]} </Actions> <p aria-live="polite" className="sr-only"> {sending ? "Checking the licence key" : ""} </p> </form> ) if (!frame) return ( <div data-slot="desktop-4" data-state={done ? "success" : current === "sending" ? "submitting" : current === "error" ? "error" : "ready"} data-feel={feelProp} role="region" aria-labelledby={`${id}-title`} className={cn("flex flex-col", className)} {...frameProps(props)}> {content} </div> ) return ( <AppWindow data-slot="desktop-4" data-state={done ? "success" : current === "sending" ? "submitting" : current === "error" ? "error" : "ready"} feel={feel} onClose={onDone} role="region" aria-labelledby={`${id}-title`} className={cn("h-[30rem] w-full max-w-md", className)} {...frameProps(props)} > <AppWindowTitleBar title={win ? `${appName} licence` : ""} icon={<KeyRound />} className="border-b-0" /> <AppWindowContent className="flex flex-col">{content}</AppWindowContent> </AppWindow> ) } export { Desktop4, type Desktop4Props, type Desktop4Licence }Update the import paths to match your project setup.
Usage
import { Desktop4 } from "@/components/ballmac/blocks/desktop-4/desktop-4"
export default function Desktop4Demo() {
return <Desktop4 feel="macos" seatsNote="One licence works on up to 3 Macs." />
}States
Pass state and the block does the rest: skeletons that match its layout, a clear empty message, an error you can retry, and announcements for screen readers.
Activating
The field goes read-only and Activate shows a spinner and "Activating…"; announced politely.
state="submitting"Activated
Who it is licensed to and on which device, with Done and, when onDeactivate is set, Deactivate.
state="success" (or pass licence)Key refused
The reason under the field, announced as an alert and linked to it; errorState.description sets it. Thrown Error messages from onActivate show the same way.
state="error"
Quality scorecard
Every applicable check passes
Measured in Chromium 153.0.8010.12 on October 19, 2026, on version 1.0.0, across 6 examples.
Passed: Accessibility scan
No serious or critical issues in light and dark, on a phone and a desktop (axe-core 4.13.0, WCAG 2.1 AA and best-practice rules).
Passed: Small screens
No sideways scrolling at 360, 390 or 1280 px wide.
Passed: Reduced motion
Every endless animation stops when the visitor asks for less motion.
Passed: Right to left
Mirrors correctly in Arabic, Hebrew, Persian and Urdu layouts (10% of elements off the mirror, limit 15%).
Passed: Clean console
No console errors or React hydration errors while loading.
Passed: Keyboard
20 tab stops, each showing a visible change when it takes focus. 3 documented keys.
Passed: Loading, empty and error
Ships submitting, success, error states, each with an example.
Passed: Automated tests
Covered by desktop-4.test.tsx.
For information: Footprint
4.0 KB of source (gzip) in 1 file. Installs lucide-react.
Automated checks find a subset of problems and do not replace testing with a screen reader and a keyboard in your own app. What they miss.
Dependencies
- npm
- lucide-react
Use with AI
A licence window. <Desktop4 appName trialDaysLeft trialLength buyUrl onActivate={async (key) => { const r = await validate(key); if (!r.ok) throw new Error(r.reason); return { licensedTo: r.email, plan: 'Pro' } }} onContinueTrial onDeactivate />. trialDaysLeft={0} switches to the trial-ended mode with Quit instead of Continue. Pass licence to open on the licensed view. With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI Desktop 4: licence activation (@ballmac/desktop-4) to this project with the shadcn MCP, then use it where it fits.
Use it for
- Activating a paid Tauri or Electron app with a licence key (Dodo Payments, Lemon Squeezy, Keygen, Paddle)
- The window shown when a trial ends
Not for
- Web subscriptions with sign-in (use billing-1 or pricing blocks)
- Entering a one-time code (use verify-1 or input-otp)
Registry JSON: https://ui.ballmac.com/r/desktop-4.json
Free to use in personal and commercial projects.
More desktop app blocks
Desktop 1: preferences window
The Settings window of a Tauri or Electron app: toolbar tabs on macOS, a side nav with setting cards on Windows. Launch at login, menu bar, appearance, a global shortcut recorder and update settings that apply as they change.
Desktop 2: menu bar and tray panel
The panel of a menu bar app on macOS or a tray flyout on Windows: a clipboard history with search, pins, a pause switch, keyboard selection and Preferences and Quit, with loading, empty and error states.
Desktop 3: software update
The update window of a desktop app, the whole cycle: checking, a new version with release notes, downloading with progress, ready to relaunch, up to date and failed. Buttons follow each platform's order and wording.
Desktop 5: permissions setup
The first-run permissions window: why each one is needed, Allow for ones not asked yet, the exact System Settings path for ones turned off, a re-check on return, and Continue once required ones are on.
Ballmac UI Pro
Need the whole screen, not one piece?
150 premium blocks (dashboards, settings, calendars, mail, shops, onboarding), a private shadcn registry and Figma tokens. Blocks that show data come with loading, empty and error states built in.
Not ready? Try three Pro blocks free
Leave your email and the blocks appear right here, ready to copy. Yours to use in any project.