Radio Group
Free, copy-and-go Radio Group components built on the SevenUI Radio Group primitive.Read the primitive docs.
Delivery frequency
Change it any time before your next box ships.
"use client";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
const frequencies = [
{ value: "weekly", label: "Weekly" },
{ value: "biweekly", label: "Every 2 weeks" },
{ value: "monthly", label: "Monthly" },
];
export default function RadioGroup01() {
return (
<div className="flex w-full max-w-md flex-col gap-3">
<div className="flex flex-col gap-1">
<p id="radio-group-01-label" className="text-sm font-medium">
Delivery frequency
</p>
<p id="radio-group-01-hint" className="text-sm text-muted-foreground">
Change it any time before your next box ships.
</p>
</div>
<RadioGroup
defaultValue="biweekly"
aria-labelledby="radio-group-01-label"
aria-describedby="radio-group-01-hint"
className="flex flex-wrap gap-x-6 gap-y-3"
>
{frequencies.map((frequency) => (
<Label key={frequency.value} className="cursor-pointer font-normal">
<RadioGroupItem value={frequency.value} />
{frequency.label}
</Label>
))}
</RadioGroup>
</div>
);
}
npx shadcn@latest add @sevenui/component/radio-group-01pnpm dlx shadcn@latest add @sevenui/component/radio-group-01yarn dlx shadcn@latest add @sevenui/component/radio-group-01bunx --bun shadcn@latest add @sevenui/component/radio-group-01Database region
Latency is measured from your location. Frankfurt reopens for new databases once capacity is added, usually within a day.
"use client";
import { Badge } from "@/components/ui/badge";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
const regions = [
{
value: "iad",
name: "Washington, D.C.",
detail: "us-east-1 · Virginia",
latency: "12 ms",
},
{
value: "sfo",
name: "San Francisco",
detail: "us-west-1 · California",
latency: "68 ms",
},
{
value: "fra",
name: "Frankfurt",
detail: "eu-central-1 · Germany",
latency: "91 ms",
unavailable: "At capacity",
},
{
value: "sin",
name: "Singapore",
detail: "ap-southeast-1 · Singapore",
latency: "214 ms",
},
];
export default function RadioGroup02() {
return (
<div className="flex w-full max-w-md flex-col gap-3">
<p id="radio-group-02-label" className="text-sm font-medium">
Database region
</p>
<RadioGroup
defaultValue="iad"
aria-labelledby="radio-group-02-label"
className="gap-0 divide-y divide-border overflow-hidden rounded-lg border border-border"
>
{regions.map((region) => (
<Label
key={region.value}
className="cursor-pointer items-start gap-3 px-4 py-3 transition-colors not-has-data-disabled:hover:bg-muted/50 has-data-checked:bg-muted/60 has-data-disabled:cursor-not-allowed"
>
<RadioGroupItem
value={region.value}
disabled={Boolean(region.unavailable)}
className="mt-0.5"
/>
<span className="flex min-w-0 flex-1 flex-col gap-1">
<span className="flex flex-wrap items-center gap-2">
{region.name}
{region.unavailable ? (
<Badge variant="outline">{region.unavailable}</Badge>
) : null}
</span>
<span className="text-xs font-normal text-muted-foreground">
{region.detail}
</span>
</span>
<span className="text-sm font-normal text-muted-foreground tabular-nums">
<span className="sr-only">Latency </span>
{region.latency}
</span>
</Label>
))}
</RadioGroup>
<p className="text-xs text-muted-foreground">
Latency is measured from your location. Frankfurt reopens for new
databases once capacity is added, usually within a day.
</p>
</div>
);
}
npx shadcn@latest add @sevenui/component/radio-group-02pnpm dlx shadcn@latest add @sevenui/component/radio-group-02yarn dlx shadcn@latest add @sevenui/component/radio-group-02bunx --bun shadcn@latest add @sevenui/component/radio-group-02"use client";
import { type FormEvent, useState } from "react";
import { CircleAlertIcon, CircleCheckIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
const meals = [
{ value: "short-rib", label: "Braised short rib, potato purée" },
{ value: "salmon", label: "Miso-glazed salmon, charred greens" },
{ value: "risotto", label: "Wild mushroom risotto (vegetarian)" },
{ value: "vegan", label: "Chef's seasonal plate (vegan)" },
];
type Status = "idle" | "error" | "success";
export default function RadioGroup03() {
const [meal, setMeal] = useState<string | null>(null);
const [status, setStatus] = useState<Status>("idle");
const invalid = status === "error";
const submit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
setStatus(meal ? "success" : "error");
};
return (
<form
noValidate
onSubmit={submit}
className="flex w-full max-w-sm flex-col gap-4"
>
<div className="flex flex-col gap-1">
<p id="radio-group-03-label" className="text-sm font-medium">
Dinner choice
</p>
<p className="text-sm text-pretty text-muted-foreground">
Required. Team offsite dinner, Tuesday, Oct 14 at 7:00 PM.
</p>
</div>
<RadioGroup
value={meal}
onValueChange={(value) => {
setMeal(value as string);
setStatus("idle");
}}
aria-labelledby="radio-group-03-label"
aria-describedby={
status !== "idle" ? "radio-group-03-status" : undefined
}
aria-invalid={invalid || undefined}
aria-required="true"
className="gap-3"
>
{meals.map((item) => (
<Label key={item.value} className="cursor-pointer font-normal">
<RadioGroupItem
value={item.value}
aria-invalid={invalid || undefined}
/>
{item.label}
</Label>
))}
</RadioGroup>
<div aria-live="polite" className="min-h-5">
{status === "error" ? (
<p
id="radio-group-03-status"
className="flex items-center gap-1.5 text-sm text-destructive"
>
<CircleAlertIcon aria-hidden="true" className="size-4 shrink-0" />
Choose a dinner option so the kitchen can plan.
</p>
) : null}
{status === "success" ? (
<p
id="radio-group-03-status"
className="flex items-center gap-1.5 text-sm text-success"
>
<CircleCheckIcon aria-hidden="true" className="size-4 shrink-0" />
RSVP saved. You can change your meal until Oct 7.
</p>
) : null}
</div>
<Button type="submit" className="self-start">
Save RSVP
</Button>
</form>
);
}
npx shadcn@latest add @sevenui/component/radio-group-03pnpm dlx shadcn@latest add @sevenui/component/radio-group-03yarn dlx shadcn@latest add @sevenui/component/radio-group-03bunx --bun shadcn@latest add @sevenui/component/radio-group-03Log retention
Read-only. Only workspace owners can edit.
"use client";
import { useEffect, useRef, useState } from "react";
import { cn } from "cn";
import { LockIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
const windows = [
{ value: "30", label: "30 days", note: "Minimum for SOC 2 evidence" },
{ value: "90", label: "90 days", note: "Recommended for most teams" },
{ value: "365", label: "1 year", note: "Required for audit trails" },
];
export default function RadioGroup04() {
const [editing, setEditing] = useState(false);
const [saved, setSaved] = useState("90");
const [draft, setDraft] = useState("90");
const groupRef = useRef<HTMLDivElement>(null);
const editRef = useRef<HTMLButtonElement>(null);
const toggled = useRef(false);
// Edit, Save and Cancel unmount themselves, so hand focus to the control
// that replaces them instead of letting it fall back to the page.
useEffect(() => {
if (!toggled.current) return;
toggled.current = false;
if (editing) {
groupRef.current
?.querySelector<HTMLElement>('[role="radio"][aria-checked="true"]')
?.focus();
} else {
editRef.current?.focus();
}
}, [editing]);
const edit = () => {
toggled.current = true;
setEditing(true);
};
const cancel = () => {
toggled.current = true;
setDraft(saved);
setEditing(false);
};
const save = () => {
toggled.current = true;
setSaved(draft);
setEditing(false);
};
return (
<div className="flex w-full max-w-sm flex-col gap-4 rounded-xl border border-border bg-card p-5 text-card-foreground">
<div className="flex items-start justify-between gap-3">
<div className="flex flex-col gap-1">
<p id="radio-group-04-label" className="text-sm font-medium">
Log retention
</p>
<p
id="radio-group-04-hint"
className="flex items-center gap-1.5 text-xs text-muted-foreground"
>
{editing ? (
"Changes apply to logs written after you save."
) : (
<>
<LockIcon aria-hidden="true" className="size-3 shrink-0" />
Read-only. Only workspace owners can edit.
</>
)}
</p>
</div>
{editing ? null : (
<Button
ref={editRef}
size="sm"
variant="outline"
onClick={edit}
>
Edit
</Button>
)}
</div>
<RadioGroup
ref={groupRef}
value={draft}
onValueChange={(value) => setDraft(value as string)}
readOnly={!editing}
aria-labelledby="radio-group-04-label"
aria-describedby="radio-group-04-hint"
className="gap-3 aria-readonly:[&_[data-slot=radio-group-item]]:opacity-60"
>
{windows.map((option) => (
<Label
key={option.value}
className={cn(
"items-start gap-3 font-normal",
editing ? "cursor-pointer" : "cursor-default",
)}
>
<RadioGroupItem value={option.value} className="mt-px" />
<span className="flex flex-col gap-1">
<span className="font-medium">{option.label}</span>
<span className="text-xs text-muted-foreground">
{option.note}
</span>
</span>
</Label>
))}
</RadioGroup>
{editing ? (
<div className="flex justify-end gap-2 border-t border-border pt-4">
<Button size="sm" variant="ghost" onClick={cancel}>
Cancel
</Button>
<Button size="sm" onClick={save} disabled={draft === saved}>
Save
</Button>
</div>
) : null}
</div>
);
}
npx shadcn@latest add @sevenui/component/radio-group-04pnpm dlx shadcn@latest add @sevenui/component/radio-group-04yarn dlx shadcn@latest add @sevenui/component/radio-group-04bunx --bun shadcn@latest add @sevenui/component/radio-group-04Interface language
"use client";
import { CheckIcon } from "lucide-react";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
const languages = [
{ value: "en-US", native: "English", english: "United States" },
{ value: "de-DE", native: "Deutsch", english: "German" },
{ value: "es-ES", native: "Español", english: "Spanish" },
{ value: "ja-JP", native: "日本語", english: "Japanese" },
{ value: "pt-BR", native: "Português", english: "Portuguese, Brazil" },
];
export default function RadioGroup05() {
return (
<div className="flex w-full max-w-xs flex-col gap-2">
<p
id="radio-group-05-label"
className="px-3 text-xs font-medium text-muted-foreground"
>
Interface language
</p>
<RadioGroup
defaultValue="de-DE"
aria-labelledby="radio-group-05-label"
className="gap-0.5"
>
{languages.map((language) => (
<Label
key={language.value}
lang={language.value}
className="cursor-pointer justify-between gap-3 rounded-md px-3 py-2.5 font-normal transition-colors hover:bg-accent has-data-checked:bg-accent has-[:focus-visible]:bg-accent"
>
<span className="flex min-w-0 flex-col gap-1">
<span className="font-medium">{language.native}</span>
<span lang="en" className="text-xs text-muted-foreground">
{language.english}
</span>
</span>
{/* The stock dot is hidden; a check fades in on the sibling peer. */}
<span className="relative flex size-5 shrink-0">
<RadioGroupItem
value={language.value}
className="size-5 border-transparent bg-transparent shadow-none dark:bg-transparent [&_[data-slot=radio-group-indicator]]:hidden"
/>
<CheckIcon
aria-hidden="true"
strokeWidth={2.5}
className="pointer-events-none absolute inset-0 m-auto size-3 scale-50 text-primary-foreground opacity-0 transition-[opacity,scale] duration-200 ease-out peer-data-checked:scale-100 peer-data-checked:opacity-100 motion-reduce:transition-none"
/>
</span>
</Label>
))}
</RadioGroup>
</div>
);
}
npx shadcn@latest add @sevenui/component/radio-group-05pnpm dlx shadcn@latest add @sevenui/component/radio-group-05yarn dlx shadcn@latest add @sevenui/component/radio-group-05bunx --bun shadcn@latest add @sevenui/component/radio-group-05Workspace layout
Applies to everyone in Northwind after a page refresh.
"use client";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
type Layout = "sidebar" | "topbar" | "split";
const layouts: { value: Layout; label: string; hint: string }[] = [
{ value: "sidebar", label: "Sidebar", hint: "Navigation on the left" },
{ value: "topbar", label: "Top bar", hint: "Navigation across the top" },
{ value: "split", label: "Split view", hint: "List and detail side by side" },
];
// A miniature wireframe of each layout, drawn with theme tokens only so it
// follows light and dark mode.
function Wireframe({ layout }: { layout: Layout }) {
const block =
"rounded-[3px] bg-muted-foreground/20 transition-colors group-has-data-checked/tile:bg-primary/25";
return (
<span
aria-hidden="true"
className="flex aspect-[4/3] w-full gap-1 overflow-hidden rounded-md border border-border bg-background p-1.5"
>
{layout === "sidebar" ? (
<>
<span className={`w-1/4 ${block}`} />
<span className="flex flex-1 flex-col gap-1">
<span className={`h-2 w-2/3 ${block}`} />
<span className="flex-1 rounded-[3px] bg-muted" />
</span>
</>
) : null}
{layout === "topbar" ? (
<span className="flex flex-1 flex-col gap-1">
<span className={`h-2.5 ${block}`} />
<span className="flex flex-1 gap-1">
<span className="flex-1 rounded-[3px] bg-muted" />
<span className="flex-1 rounded-[3px] bg-muted" />
</span>
</span>
) : null}
{layout === "split" ? (
<>
<span className="flex w-2/5 flex-col gap-1">
<span className={`h-2 ${block}`} />
<span className={`h-2 ${block}`} />
<span className={`h-2 ${block}`} />
</span>
<span className="flex-1 rounded-[3px] bg-muted" />
</>
) : null}
</span>
);
}
export default function RadioGroup06() {
return (
<div className="flex w-full max-w-md flex-col gap-3">
<div className="flex flex-col gap-1">
<p id="radio-group-06-label" className="text-sm font-medium">
Workspace layout
</p>
<p id="radio-group-06-hint" className="text-sm text-muted-foreground">
Applies to everyone in Northwind after a page refresh.
</p>
</div>
<RadioGroup
defaultValue="sidebar"
aria-labelledby="radio-group-06-label"
aria-describedby="radio-group-06-hint"
className="grid-cols-3 gap-2 sm:gap-3"
>
{layouts.map((layout) => (
<Label
key={layout.value}
className="group/tile min-w-0 cursor-pointer flex-col items-stretch gap-2 rounded-lg p-1.5 font-normal transition-colors hover:bg-muted/50 has-[:focus-visible]:ring-3 has-[:focus-visible]:ring-ring/50"
>
<span className="rounded-md ring-2 ring-transparent transition-shadow group-has-data-checked/tile:ring-primary">
<Wireframe layout={layout.value} />
</span>
<span className="flex flex-col items-center gap-1.5 text-center sm:flex-row sm:items-start sm:gap-2 sm:text-left">
<RadioGroupItem
value={layout.value}
className="mt-px focus-visible:ring-0"
/>
<span className="flex min-w-0 flex-col gap-1">
<span className="font-medium">{layout.label}</span>
<span className="hidden text-xs leading-snug text-muted-foreground sm:block">
{layout.hint}
</span>
</span>
</span>
</Label>
))}
</RadioGroup>
</div>
);
}
npx shadcn@latest add @sevenui/component/radio-group-06pnpm dlx shadcn@latest add @sevenui/component/radio-group-06yarn dlx shadcn@latest add @sevenui/component/radio-group-06bunx --bun shadcn@latest add @sevenui/component/radio-group-06Request review from
Maya gets notified
"use client";
import { useState } from "react";
import {
Avatar,
AvatarBadge,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
const reviewers = [
{
value: "maya",
name: "Maya Lindqvist",
initials: "ML",
role: "Frontend lead",
load: "1 open review",
online: true,
},
{
value: "tomas",
name: "Tomás Ferreira",
initials: "TF",
role: "Design systems",
load: "4 open reviews",
online: true,
},
{
value: "priya",
name: "Priya Raman",
initials: "PR",
role: "Accessibility",
load: "2 open reviews",
online: false,
},
];
export default function RadioGroup07() {
const [reviewer, setReviewer] = useState("maya");
const selected = reviewers.find((person) => person.value === reviewer);
return (
<div className="flex w-full max-w-sm flex-col gap-3">
<div className="flex items-baseline justify-between gap-3">
<p id="radio-group-07-label" className="text-sm font-medium">
Request review from
</p>
<p
aria-live="polite"
className="truncate text-xs text-muted-foreground"
>
{selected ? `${selected.name.split(" ")[0]} gets notified` : null}
</p>
</div>
<RadioGroup
value={reviewer}
onValueChange={(value) => setReviewer(value as string)}
aria-labelledby="radio-group-07-label"
className="gap-1"
>
{reviewers.map((person) => (
<Label
key={person.value}
className="cursor-pointer gap-3 rounded-lg border border-transparent px-2.5 py-2 font-normal transition-colors hover:bg-muted/50 has-data-checked:border-border has-data-checked:bg-card"
>
<Avatar>
<AvatarImage src="/placeholder.svg" alt="" />
<AvatarFallback>{person.initials}</AvatarFallback>
<AvatarBadge
className={
person.online ? "bg-success" : "bg-muted-foreground/60"
}
/>
</Avatar>
<span className="flex min-w-0 flex-1 flex-col gap-1">
<span className="truncate font-medium">{person.name}</span>
<span className="text-xs leading-snug text-muted-foreground sm:truncate">
{person.role} · {person.online ? "Online" : "Away"} ·{" "}
{person.load}
</span>
</span>
<RadioGroupItem value={person.value} />
</Label>
))}
</RadioGroup>
</div>
);
}
npx shadcn@latest add @sevenui/component/radio-group-07pnpm dlx shadcn@latest add @sevenui/component/radio-group-07yarn dlx shadcn@latest add @sevenui/component/radio-group-07bunx --bun shadcn@latest add @sevenui/component/radio-group-07Delivery method
"use client";
import { useState } from "react";
import { CalendarClockIcon, StoreIcon, TruckIcon } from "lucide-react";
import { cn } from "cn";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
NativeSelect,
NativeSelectOption,
} from "@/components/ui/native-select";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
const methods = [
{
value: "standard",
label: "Standard delivery",
summary: "Arrives Thu, Oct 2",
price: "Free",
icon: TruckIcon,
},
{
value: "scheduled",
label: "Scheduled delivery",
summary: "Pick a 2-hour window",
price: "$6.00",
icon: CalendarClockIcon,
},
{
value: "pickup",
label: "Store pickup",
summary: "Ready in 2 hours at Market Street",
price: "Free",
icon: StoreIcon,
},
];
const timeWindows = [
"Tue, Sep 30 · 8–10 AM",
"Tue, Sep 30 · 6–8 PM",
"Wed, Oct 1 · 10 AM–12 PM",
"Wed, Oct 1 · 4–6 PM",
];
function MethodDetails({ value }: { value: string }) {
if (value === "scheduled") {
return (
<div className="flex flex-col gap-2">
<Label htmlFor="radio-group-08-window">Delivery window</Label>
<NativeSelect id="radio-group-08-window" className="w-full">
{timeWindows.map((slot) => (
<NativeSelectOption key={slot} value={slot}>
{slot}
</NativeSelectOption>
))}
</NativeSelect>
</div>
);
}
if (value === "pickup") {
return (
<div className="flex flex-col gap-2">
<Label htmlFor="radio-group-08-pickup">Who's picking up?</Label>
<Input
id="radio-group-08-pickup"
autoComplete="name"
placeholder="Full name on photo ID"
/>
</div>
);
}
return null;
}
export default function RadioGroup08() {
const [method, setMethod] = useState("scheduled");
return (
<div className="flex w-full max-w-md flex-col gap-3">
<p id="radio-group-08-label" className="text-sm font-medium">
Delivery method
</p>
<RadioGroup
value={method}
onValueChange={(value) => setMethod(value as string)}
aria-labelledby="radio-group-08-label"
className="gap-2"
>
{methods.map((item) => {
const Icon = item.icon;
const checked = method === item.value;
const hasDetails = item.value !== "standard";
return (
<div
key={item.value}
className={cn(
"rounded-xl border bg-card text-card-foreground transition-colors",
checked
? "border-primary/50 bg-primary/5 dark:bg-primary/10"
: "border-border",
)}
>
<Label className="cursor-pointer items-center gap-3 p-4 font-normal">
<RadioGroupItem value={item.value} />
<Icon
aria-hidden="true"
className={cn(
"size-4 shrink-0 transition-colors",
checked ? "text-foreground" : "text-muted-foreground",
)}
/>
<span className="flex min-w-0 flex-1 flex-col gap-1">
<span className="font-medium">{item.label}</span>
<span className="text-xs text-muted-foreground">
{item.summary}
</span>
</span>
<span className="text-sm tabular-nums">{item.price}</span>
</Label>
{hasDetails ? (
// Collapsed panels stay mounted so the height can animate,
// and `inert` keeps their fields out of the tab order.
<fieldset
aria-label={`${item.label} details`}
inert={!checked}
// Keep arrow keys inside the fields instead of letting the
// radio group treat them as option navigation.
onKeyDown={(event) => event.stopPropagation()}
className={cn(
"m-0 grid min-w-0 border-0 p-0 transition-[grid-template-rows,opacity] duration-300 ease-out motion-reduce:transition-none",
checked
? "grid-rows-[1fr] opacity-100"
: "grid-rows-[0fr] opacity-0",
)}
>
<div className="overflow-hidden">
<div className="px-4 pb-4 sm:pl-11">
<MethodDetails value={item.value} />
</div>
</div>
</fieldset>
) : null}
</div>
);
})}
</RadioGroup>
</div>
);
}
npx shadcn@latest add @sevenui/component/radio-group-08pnpm dlx shadcn@latest add @sevenui/component/radio-group-08yarn dlx shadcn@latest add @sevenui/component/radio-group-08bunx --bun shadcn@latest add @sevenui/component/radio-group-08Billing cycle
Team plan for 8 seats. Switch cycles any time from Billing.
- 8 seats × $144
- $1,152
- Works out to
- $12 per seat / month
- Due today
- $1,152
Renews Sep 25, 2027. Cancel any time before then.
"use client";
import * as React from "react";
import { CircleCheckIcon } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
type Cycle = "monthly" | "yearly";
const seats = 8;
const cycles: {
value: Cycle;
label: string;
perSeat: number;
unit: string;
renews: string;
savings?: string;
}[] = [
{
value: "monthly",
label: "Monthly",
perSeat: 15,
unit: "per seat / month",
renews: "Renews Oct 25, 2026",
},
{
value: "yearly",
label: "Yearly",
perSeat: 144,
unit: "per seat / year",
renews: "Renews Sep 25, 2027",
savings: "Save 20%",
},
];
const currency = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
maximumFractionDigits: 0,
});
export default function RadioGroup09() {
const [cycle, setCycle] = React.useState<Cycle>("yearly");
// The cycle the team is paying for, once "Confirm and pay" has run.
const [paid, setPaid] = React.useState<Cycle | null>(null);
const selected = cycles.find((item) => item.value === cycle) ?? cycles[0];
const monthlyEquivalent = cycle === "yearly" ? selected.perSeat / 12 : null;
return (
<div className="flex w-full max-w-md flex-col gap-4 rounded-xl border border-border bg-card p-4 text-card-foreground">
<div className="flex flex-col gap-1">
<h3 id="radio-group-09-title" className="text-sm font-semibold">
Billing cycle
</h3>
<p className="text-sm text-muted-foreground">
Team plan for {seats} seats. Switch cycles any time from Billing.
</p>
</div>
<RadioGroup
aria-labelledby="radio-group-09-title"
value={cycle}
onValueChange={(value) => setCycle(value as Cycle)}
className="grid-cols-1 gap-2 min-[400px]:grid-cols-2"
>
{cycles.map((item) => (
<Label
key={item.value}
className="cursor-pointer flex-col items-stretch gap-3 rounded-lg border border-border p-3 font-normal transition-colors hover:bg-muted/50 has-data-checked:border-primary has-data-checked:bg-primary/5 dark:has-data-checked:bg-primary/10"
>
<span className="flex h-6 items-center justify-between gap-2">
<span className="flex items-center gap-2 font-medium">
<RadioGroupItem value={item.value} />
{item.label}
</span>
{item.savings ? (
<Badge variant="secondary">{item.savings}</Badge>
) : null}
</span>
<span className="flex flex-col gap-1">
<span className="text-xl font-semibold tabular-nums">
{currency.format(item.perSeat)}
</span>
<span className="text-xs text-muted-foreground">{item.unit}</span>
</span>
</Label>
))}
</RadioGroup>
<dl className="flex flex-col gap-2 text-sm">
<div className="flex justify-between gap-3">
<dt className="text-muted-foreground">
{seats} seats × {currency.format(selected.perSeat)}
</dt>
<dd className="tabular-nums">
{currency.format(seats * selected.perSeat)}
</dd>
</div>
{monthlyEquivalent ? (
<div className="flex justify-between gap-3">
<dt className="text-muted-foreground">Works out to</dt>
<dd className="tabular-nums">
{currency.format(monthlyEquivalent)} per seat / month
</dd>
</div>
) : null}
<div className="flex justify-between gap-3 border-t border-border pt-2 font-medium">
<dt>Due today</dt>
<dd className="tabular-nums" aria-live="polite">
{currency.format(seats * selected.perSeat)}
</dd>
</div>
</dl>
<div className="flex flex-col gap-2">
<Button
className="w-full"
disabled={paid === cycle}
onClick={() => setPaid(cycle)}
>
{paid === null
? "Confirm and pay"
: paid === cycle
? `Paid · ${selected.label.toLowerCase()} billing`
: `Switch to ${selected.label.toLowerCase()} billing`}
</Button>
<p
aria-live="polite"
className="flex items-center justify-center gap-1.5 text-center text-xs text-muted-foreground"
>
{paid === cycle ? (
<>
<CircleCheckIcon
aria-hidden="true"
className="size-3.5 shrink-0 text-success"
/>
Payment received. {selected.renews}.
</>
) : (
`${selected.renews}. Cancel any time before then.`
)}
</p>
</div>
</div>
);
}
npx shadcn@latest add @sevenui/component/radio-group-09pnpm dlx shadcn@latest add @sevenui/component/radio-group-09yarn dlx shadcn@latest add @sevenui/component/radio-group-09bunx --bun shadcn@latest add @sevenui/component/radio-group-09"use client";
import * as React from "react";
import { Check, CircleAlert, CircleCheck, Minus } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
const roles = [
{ value: "viewer", label: "Viewer", summary: "Can view and comment" },
{ value: "editor", label: "Editor", summary: "Can edit projects" },
{ value: "admin", label: "Admin", summary: "Full workspace access" },
];
const permissions = [
{ label: "View projects and comment", roles: ["viewer", "editor", "admin"] },
{ label: "Create and edit projects", roles: ["editor", "admin"] },
{ label: "Publish to production", roles: ["editor", "admin"] },
{ label: "Manage members and billing", roles: ["admin"] },
];
export default function RadioGroup10() {
const [role, setRole] = React.useState("editor");
const [email, setEmail] = React.useState("");
const [status, setStatus] = React.useState<
{ kind: "idle" } | { kind: "error" } | { kind: "sent"; to: string; role: string }
>({ kind: "idle" });
const selected = roles.find((item) => item.value === role) ?? roles[0];
const invalid = status.kind === "error";
return (
<form
noValidate
onSubmit={(event) => {
event.preventDefault();
const input = event.currentTarget.elements.namedItem("email") as HTMLInputElement;
if (!email.trim() || !input.checkValidity()) {
setStatus({ kind: "error" });
input.focus();
return;
}
setStatus({ kind: "sent", to: email.trim(), role: selected.label.toLowerCase() });
setEmail("");
}}
className="flex w-full max-w-md flex-col gap-5 rounded-xl border border-border bg-card p-4 text-card-foreground"
>
<div className="flex flex-col gap-1">
<h3 className="text-sm font-semibold">Invite to Northwind</h3>
<p className="text-sm text-muted-foreground">
They will get an email with a link to join this workspace.
</p>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="radio-group-10-email">Email address</Label>
<Input
id="radio-group-10-email"
name="email"
type="email"
required
value={email}
onChange={(event) => {
setEmail(event.target.value);
if (status.kind !== "idle") setStatus({ kind: "idle" });
}}
aria-invalid={invalid || undefined}
aria-describedby={invalid ? "radio-group-10-error" : undefined}
placeholder="daniel@northwind.dev"
/>
{invalid ? (
<p id="radio-group-10-error" className="flex items-center gap-1.5 text-xs text-destructive">
<CircleAlert aria-hidden="true" className="size-3.5 shrink-0" />
Enter a valid email address.
</p>
) : null}
</div>
<div className="flex flex-col gap-2">
<span id="radio-group-10-role" className="text-sm font-medium">
Role
</span>
<RadioGroup
aria-labelledby="radio-group-10-role"
value={role}
onValueChange={(value) => setRole(value as string)}
className="grid grid-cols-3 gap-1 rounded-lg bg-muted p-1"
>
{roles.map((item) => (
<Label
key={item.value}
className="relative cursor-pointer justify-center rounded-md px-2 py-2 text-muted-foreground transition-colors hover:text-foreground has-data-checked:bg-background has-data-checked:text-foreground has-data-checked:shadow-sm has-focus-visible:ring-3 has-focus-visible:ring-ring/50"
>
<RadioGroupItem value={item.value} className="sr-only absolute" />
{item.label}
</Label>
))}
</RadioGroup>
<p className="text-xs text-muted-foreground" aria-live="polite">
{selected.summary}
</p>
</div>
<ul className="flex flex-col gap-2 rounded-lg border border-border p-3 text-sm">
{permissions.map((permission) => {
const allowed = permission.roles.includes(role);
return (
<li
key={permission.label}
className={allowed ? "flex items-center gap-2" : "flex items-center gap-2 text-muted-foreground"}
>
{allowed ? (
<Check aria-hidden="true" className="size-4 shrink-0 text-primary" />
) : (
<Minus aria-hidden="true" className="size-4 shrink-0" />
)}
<span className={allowed ? undefined : "line-through decoration-muted-foreground/40"}>
{permission.label}
</span>
<span className="sr-only">{allowed ? "(allowed)" : "(not allowed)"}</span>
</li>
);
})}
</ul>
<div className="flex flex-col gap-2">
<Button type="submit" className="w-full">
Send invite as {selected.label.toLowerCase()}
</Button>
<p aria-live="polite" className="flex min-h-4 items-center justify-center gap-1.5 text-center text-xs text-muted-foreground">
{status.kind === "sent" ? (
<>
<CircleCheck aria-hidden="true" className="size-3.5 shrink-0 text-success" />
Invite sent to {status.to} as {status.role}.
</>
) : null}
</p>
</div>
</form>
);
}
npx shadcn@latest add @sevenui/component/radio-group-10pnpm dlx shadcn@latest add @sevenui/component/radio-group-10yarn dlx shadcn@latest add @sevenui/component/radio-group-10bunx --bun shadcn@latest add @sevenui/component/radio-group-10"use client";
import * as React from "react";
import { GitMergeIcon, Undo2Icon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
type Strategy = "merge" | "squash" | "rebase";
const strategies: {
value: Strategy;
label: string;
description: string;
action: string;
done: string;
}[] = [
{
value: "merge",
label: "Create a merge commit",
description: "Keeps all 3 commits and adds a merge commit on main.",
action: "Merge pull request",
done: "Merged into main with a merge commit.",
},
{
value: "squash",
label: "Squash and merge",
description: "Combines all 3 commits into one commit on main.",
action: "Squash and merge",
done: "Squashed into one commit on main.",
},
{
value: "rebase",
label: "Rebase and merge",
description: "Replays all 3 commits on top of main, no merge commit.",
action: "Rebase and merge",
done: "Rebased onto main, no merge commit.",
},
];
const branchCommits = [
{ sha: "a41c9e2", message: "Add CSV export endpoint" },
{ sha: "7d03b18", message: "Stream rows instead of buffering" },
{ sha: "e5f2a07", message: "Fix header order in export" },
];
// The commits that land on main for each strategy, newest first.
const results: Record<
Strategy,
{ sha: string; message: string; merge?: boolean }[]
> = {
merge: [
{
sha: "c90b4d1",
message: "Merge branch 'billing-export' into main",
merge: true,
},
...branchCommits.slice().reverse(),
],
squash: [{ sha: "f18e6c3", message: "Add billing CSV export" }],
rebase: branchCommits
.slice()
.reverse()
.map((commit, index) => ({
message: commit.message,
sha: ["3b7e0a9", "96d1f4c", "0ea52b8"][index],
})),
};
export default function RadioGroup11() {
const [strategy, setStrategy] = React.useState<Strategy>("squash");
const [merged, setMerged] = React.useState(false);
const selected =
strategies.find((item) => item.value === strategy) ?? strategies[0];
const history = results[strategy];
return (
<form
onSubmit={(event) => {
event.preventDefault();
setMerged(true);
}}
className="flex w-full max-w-md flex-col gap-4 rounded-xl border border-border bg-card p-4 text-card-foreground"
>
<div className="flex flex-col gap-1">
<h3 id="radio-group-11-title" className="text-sm font-semibold">
Merge billing-export into main
</h3>
<p className="text-sm text-muted-foreground" aria-live="polite">
{merged
? selected.done
: "billing-export is 3 commits ahead. All checks have passed."}
</p>
</div>
<RadioGroup
aria-labelledby="radio-group-11-title"
value={strategy}
onValueChange={(value) => setStrategy(value as Strategy)}
disabled={merged}
className="gap-3"
>
{strategies.map((item) => (
<Label
key={item.value}
className="cursor-pointer items-start gap-3 font-normal has-data-disabled:cursor-default"
>
<RadioGroupItem value={item.value} className="mt-px" />
<span className="flex flex-col gap-1">
<span className="font-medium">{item.label}</span>
<span className="text-xs leading-snug text-muted-foreground">
{item.description}
</span>
</span>
</Label>
))}
</RadioGroup>
<div className="flex flex-col gap-2 rounded-lg bg-muted/50 p-3">
<p
id="radio-group-11-preview"
className="text-xs font-medium text-muted-foreground"
>
Resulting history on main
</p>
<ol
aria-labelledby="radio-group-11-preview"
aria-live="polite"
className="flex flex-col"
>
{history.map((commit, index) => (
<li key={commit.sha} className="group/commit flex gap-3">
<span
aria-hidden="true"
className="flex w-3 flex-col items-center"
>
<span
className={
commit.merge
? "mt-1 size-3 shrink-0 rounded-full border-2 border-primary bg-card"
: "mt-1 size-3 shrink-0 rounded-full bg-primary"
}
/>
{index < history.length - 1 ? (
<span className="w-px flex-1 bg-border" />
) : null}
</span>
<span className="flex min-w-0 flex-1 items-baseline justify-between gap-3 pb-2.5 text-sm group-last/commit:pb-0">
<span className="truncate">{commit.message}</span>
<code className="shrink-0 font-mono text-xs text-muted-foreground">
{commit.sha}
</code>
</span>
</li>
))}
</ol>
</div>
{merged ? (
// Distinct keys stop React from reusing the submit button's node, which
// would otherwise turn this click into a second form submit.
<Button
key="revert"
type="button"
variant="outline"
className="w-full"
onClick={() => setMerged(false)}
>
<Undo2Icon aria-hidden="true" />
Revert merge
</Button>
) : (
<Button key="merge" type="submit" className="w-full">
<GitMergeIcon aria-hidden="true" />
{selected.action}
</Button>
)}
</form>
);
}
npx shadcn@latest add @sevenui/component/radio-group-11pnpm dlx shadcn@latest add @sevenui/component/radio-group-11yarn dlx shadcn@latest add @sevenui/component/radio-group-11bunx --bun shadcn@latest add @sevenui/component/radio-group-11Step 2 of 4Workspace setup
How do you plan to use Relay?
We will set up templates that fit. You can change this later.
"use client";
import * as React from "react";
import { BookOpen, Briefcase, CircleCheck, GraduationCap, User } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Progress } from "@/components/ui/progress";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
const useCases = [
{
value: "work",
label: "Work",
description: "Plan projects with my team",
icon: Briefcase,
},
{
value: "personal",
label: "Personal",
description: "Organize my own life",
icon: User,
},
{
value: "school",
label: "School",
description: "Track classes and assignments",
icon: GraduationCap,
},
{
value: "writing",
label: "Writing",
description: "Draft notes and long-form docs",
icon: BookOpen,
},
];
function StepHeader({ step }: { step: number }) {
return (
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between text-xs text-muted-foreground">
<span>Step {step} of 4</span>
<span>Workspace setup</span>
</div>
<Progress value={step * 25} aria-label="Onboarding progress" />
</div>
);
}
export default function RadioGroup12() {
const [useCase, setUseCase] = React.useState<string | null>(null);
const [step, setStep] = React.useState(2);
const chosen = useCases.find((item) => item.value === useCase);
if (step !== 2) {
// The neighbouring steps are stubs so Back, Skip and Continue lead
// somewhere and the answer on step 2 survives the round trip.
return (
<div className="flex w-full max-w-md flex-col gap-6 rounded-xl border border-border bg-card p-5 text-card-foreground">
<StepHeader step={step} />
<div role="status" className="flex flex-col gap-1">
<h3 className="text-lg font-semibold text-balance">
{step === 1 ? "Name your workspace" : chosen ? "Templates added" : "Use case skipped"}
</h3>
<p className="flex items-center gap-1.5 text-sm text-muted-foreground">
{step === 1 ? (
<>
<CircleCheck aria-hidden="true" className="size-4 shrink-0 text-success" />
Northwind is saved as your workspace name.
</>
) : chosen ? (
`Templates for ${chosen.label.toLowerCase()} are ready in your sidebar.`
) : (
"No templates added. You can pick a use case later in Settings."
)}
</p>
</div>
<div className="flex items-center justify-between gap-2">
{step === 1 ? (
<span />
) : (
<Button variant="ghost" onClick={() => setStep(2)}>
Back
</Button>
)}
{step === 1 ? <Button onClick={() => setStep(2)}>Continue</Button> : null}
</div>
</div>
);
}
return (
<div className="flex w-full max-w-md flex-col gap-6 rounded-xl border border-border bg-card p-5 text-card-foreground">
<StepHeader step={2} />
<div className="flex flex-col gap-1">
<h3 id="radio-group-12-title" className="text-lg font-semibold text-balance">
How do you plan to use Relay?
</h3>
<p className="text-sm text-muted-foreground">
We will set up templates that fit. You can change this later.
</p>
</div>
<RadioGroup
aria-labelledby="radio-group-12-title"
value={useCase}
onValueChange={(value) => setUseCase(value as string)}
className="grid grid-cols-1 gap-3 min-[400px]:grid-cols-2"
>
{useCases.map((item) => (
<Label
key={item.value}
className="relative cursor-pointer flex-col items-start gap-3 rounded-lg border border-border p-3 font-normal transition-colors hover:bg-muted/50 has-data-checked:border-primary has-data-checked:bg-primary/5"
>
<RadioGroupItem value={item.value} className="absolute top-3 right-3" />
<span className="flex size-9 items-center justify-center rounded-md bg-muted">
<item.icon aria-hidden="true" className="size-4.5" />
</span>
<span className="flex flex-col gap-1 pr-5">
<span className="font-medium">{item.label}</span>
<span className="text-xs leading-snug text-muted-foreground">
{item.description}
</span>
</span>
</Label>
))}
</RadioGroup>
<div className="flex items-center justify-between gap-2">
<Button variant="ghost" onClick={() => setStep(1)}>
Back
</Button>
<div className="flex gap-2">
<Button
variant="outline"
onClick={() => {
setUseCase(null);
setStep(3);
}}
>
Skip
</Button>
<Button disabled={useCase === null} onClick={() => setStep(3)}>
Continue
</Button>
</div>
</div>
</div>
);
}
npx shadcn@latest add @sevenui/component/radio-group-12pnpm dlx shadcn@latest add @sevenui/component/radio-group-12yarn dlx shadcn@latest add @sevenui/component/radio-group-12bunx --bun shadcn@latest add @sevenui/component/radio-group-12"use client";
import * as React from "react";
import { BadgePercent } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Textarea } from "@/components/ui/textarea";
const reasons = [
{ value: "price", label: "It's too expensive" },
{ value: "features", label: "Missing a feature I need" },
{ value: "switching", label: "Switching to another tool" },
{ value: "usage", label: "I don't use it enough" },
{ value: "other", label: "Something else" },
];
export default function RadioGroup13() {
const [reason, setReason] = React.useState<string | null>(null);
const [details, setDetails] = React.useState("");
const [status, setStatus] = React.useState<
"survey" | "kept" | "stayed" | "canceled"
>("survey");
if (status !== "survey") {
return (
<div
role="status"
className="flex w-full max-w-md flex-col gap-3 rounded-xl border border-border bg-card p-5 text-card-foreground"
>
<h3 className="text-sm font-semibold">
{status === "canceled"
? "Your plan has been canceled"
: status === "kept"
? "Discount applied"
: "You're still on Pro"}
</h3>
<p className="text-sm text-muted-foreground">
{status === "canceled"
? "Pro features stay active until Oct 31. You can resubscribe anytime from Billing."
: status === "kept"
? "You'll pay $6/month for the next 3 months. Thanks for staying with us."
: "Nothing changed. Your next invoice is $12 on Oct 31."}
</p>
<Button
variant="outline"
className="self-start"
onClick={() => {
setStatus("survey");
setReason(null);
setDetails("");
}}
>
Back to billing
</Button>
</div>
);
}
const needsDetails = reason === "other" || reason === "features";
const canSubmit = reason !== null && (!needsDetails || details.trim().length > 0);
return (
<form
onSubmit={(event) => {
event.preventDefault();
if (canSubmit) setStatus("canceled");
}}
className="flex w-full max-w-md flex-col gap-5 rounded-xl border border-border bg-card p-5 text-card-foreground"
>
<div className="flex flex-col gap-1">
<h3 id="radio-group-13-title" className="text-base font-semibold">
Before you cancel Pro
</h3>
<p className="text-sm text-muted-foreground">
What's the main reason you're leaving? Your answer goes straight to our product team.
</p>
</div>
<RadioGroup
aria-labelledby="radio-group-13-title"
aria-required="true"
value={reason}
onValueChange={(value) => setReason(value as string)}
className="gap-3.5"
>
{reasons.map((item) => (
<Label key={item.value} className="cursor-pointer font-normal">
<RadioGroupItem value={item.value} />
{item.label}
</Label>
))}
</RadioGroup>
{reason === "price" && (
<div className="flex gap-3 rounded-lg border border-border bg-muted/50 p-3">
<BadgePercent aria-hidden="true" className="mt-0.5 size-4 shrink-0 text-primary" />
<div className="flex flex-col gap-2">
<p className="text-sm">
<span className="font-medium">Stay for 50% off.</span>{" "}
<span className="text-muted-foreground">
Keep Pro for $6/month for the next 3 months.
</span>
</p>
<Button
type="button"
size="sm"
variant="secondary"
className="self-start"
onClick={() => setStatus("kept")}
>
Apply discount
</Button>
</div>
</div>
)}
{needsDetails && (
<div className="flex flex-col gap-2">
<Label htmlFor="radio-group-13-details">
{reason === "features" ? "Which feature were you missing?" : "Tell us more"}
</Label>
<Textarea
id="radio-group-13-details"
value={details}
onChange={(event) => setDetails(event.target.value)}
placeholder={
reason === "features"
? "For example: recurring tasks, Gantt view, SSO"
: "What could we have done better?"
}
className="min-h-20"
/>
</div>
)}
<div className="flex flex-col-reverse gap-2 min-[400px]:flex-row min-[400px]:justify-end">
<Button type="button" variant="ghost" onClick={() => setStatus("stayed")}>
Keep my plan
</Button>
<Button type="submit" variant="destructive" disabled={!canSubmit}>
Cancel subscription
</Button>
</div>
</form>
);
}
npx shadcn@latest add @sevenui/component/radio-group-13pnpm dlx shadcn@latest add @sevenui/component/radio-group-13yarn dlx shadcn@latest add @sevenui/component/radio-group-13bunx --bun shadcn@latest add @sevenui/component/radio-group-13"use client";
import * as React from "react";
import { CalendarCheck, Clock, Globe, Video } from "lucide-react";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
const days = [
{ value: "2026-09-28", weekday: "Mon", date: "28", long: "Monday, September 28" },
{ value: "2026-09-29", weekday: "Tue", date: "29", long: "Tuesday, September 29" },
{ value: "2026-09-30", weekday: "Wed", date: "30", long: "Wednesday, September 30" },
{ value: "2026-10-01", weekday: "Thu", date: "1", long: "Thursday, October 1" },
{ value: "2026-10-02", weekday: "Fri", date: "2", long: "Friday, October 2" },
];
const times = ["9:00 AM", "9:30 AM", "10:30 AM", "11:00 AM", "1:30 PM", "2:00 PM", "3:30 PM", "4:00 PM"];
// Slots already taken, keyed by day.
const booked: Record<string, string[]> = {
"2026-09-28": ["9:00 AM", "9:30 AM", "2:00 PM"],
"2026-09-29": ["10:30 AM", "11:00 AM", "1:30 PM", "4:00 PM"],
"2026-09-30": ["3:30 PM"],
"2026-10-01": [...times],
"2026-10-02": ["9:00 AM", "4:00 PM"],
};
export default function RadioGroup14() {
const [day, setDay] = React.useState(days[1].value);
const [time, setTime] = React.useState<string | null>(null);
const [confirmed, setConfirmed] = React.useState(false);
const selectedDay = days.find((item) => item.value === day) ?? days[0];
const taken = booked[day] ?? [];
const openCount = times.length - taken.length;
if (confirmed && time) {
return (
<div
role="status"
className="flex w-full max-w-md flex-col items-center gap-3 rounded-xl border border-border bg-card p-6 text-center text-card-foreground"
>
<span className="flex size-10 items-center justify-center rounded-full bg-primary/10 text-primary">
<CalendarCheck aria-hidden="true" className="size-5" />
</span>
<h3 className="text-base font-semibold">You're booked with Priya</h3>
<p className="text-sm text-muted-foreground">
{selectedDay.long} at {time} (PDT). A calendar invite with the video link is on its way
to your inbox.
</p>
<Button
variant="outline"
onClick={() => {
setConfirmed(false);
setTime(null);
}}
>
Reschedule
</Button>
</div>
);
}
return (
<form
onSubmit={(event) => {
event.preventDefault();
if (time) setConfirmed(true);
}}
className="flex w-full max-w-md flex-col gap-5 rounded-xl border border-border bg-card p-4 text-card-foreground"
>
<div className="flex items-start gap-3">
<Avatar>
<AvatarFallback>PS</AvatarFallback>
</Avatar>
<div className="flex min-w-0 flex-col gap-1">
<h3 className="text-sm font-semibold">Product demo with Priya Shah</h3>
<div className="flex flex-wrap gap-x-3 gap-y-1 text-xs text-muted-foreground">
<span className="inline-flex items-center gap-1">
<Clock aria-hidden="true" className="size-3.5" />
30 min
</span>
<span className="inline-flex items-center gap-1">
<Video aria-hidden="true" className="size-3.5" />
Google Meet
</span>
<span className="inline-flex items-center gap-1">
<Globe aria-hidden="true" className="size-3.5" />
Pacific Time (PDT)
</span>
</div>
</div>
</div>
<div className="flex flex-col gap-2">
<span id="radio-group-14-day" className="text-sm font-medium">
Pick a day
</span>
<RadioGroup
aria-labelledby="radio-group-14-day"
value={day}
onValueChange={(value) => {
setDay(value as string);
setTime(null);
}}
className="grid grid-cols-5 gap-1.5"
>
{days.map((item) => {
const full = (booked[item.value] ?? []).length >= times.length;
return (
<Label
key={item.value}
className="cursor-pointer flex-col gap-1 rounded-lg border border-border py-2 font-normal transition-colors hover:bg-muted/50 has-focus-visible:ring-3 has-focus-visible:ring-ring/50 has-data-checked:border-primary has-data-checked:bg-primary has-data-checked:text-primary-foreground has-data-disabled:cursor-not-allowed has-data-disabled:hover:bg-transparent"
>
<RadioGroupItem
value={item.value}
disabled={full}
aria-label={full ? `${item.long}, fully booked` : item.long}
className="sr-only absolute"
/>
<span className="text-xs opacity-80">{item.weekday}</span>
<span className="text-base font-semibold tabular-nums">{item.date}</span>
</Label>
);
})}
</RadioGroup>
</div>
<div className="flex flex-col gap-2">
<div className="flex items-baseline justify-between gap-2">
<span id="radio-group-14-time" className="text-sm font-medium">
Available times
</span>
<span className="text-xs text-muted-foreground" aria-live="polite">
{openCount} open on {selectedDay.weekday}
</span>
</div>
<RadioGroup
aria-labelledby="radio-group-14-time"
value={time}
onValueChange={(value) => setTime(value as string)}
className="grid grid-cols-2 gap-1.5 min-[400px]:grid-cols-4"
>
{times.map((slot) => {
const isTaken = taken.includes(slot);
return (
<Label
key={slot}
className="cursor-pointer justify-center rounded-md border border-border py-2 text-xs font-medium tabular-nums transition-colors hover:border-primary/50 has-focus-visible:ring-3 has-focus-visible:ring-ring/50 has-data-checked:border-primary has-data-checked:bg-primary/10 has-data-checked:text-foreground has-data-disabled:cursor-not-allowed has-data-disabled:border-dashed has-data-disabled:text-muted-foreground has-data-disabled:line-through has-data-disabled:hover:border-border"
>
<RadioGroupItem value={slot} disabled={isTaken} className="sr-only absolute" />
{slot}
{isTaken && <span className="sr-only">(booked)</span>}
</Label>
);
})}
</RadioGroup>
</div>
<div className="flex flex-col gap-3 border-t border-border pt-4 min-[400px]:flex-row min-[400px]:items-center min-[400px]:justify-between">
<p className="text-xs text-muted-foreground">
{time ? `${selectedDay.long}, ${time}` : "Select a time to continue"}
</p>
<Button type="submit" disabled={!time}>
Confirm booking
</Button>
</div>
</form>
);
}
npx shadcn@latest add @sevenui/component/radio-group-14pnpm dlx shadcn@latest add @sevenui/component/radio-group-14yarn dlx shadcn@latest add @sevenui/component/radio-group-14bunx --bun shadcn@latest add @sevenui/component/radio-group-14