The composer's control-pill family - PillSelect (hover-card value picker), TogglePill, NumberPill, and ActionPopover. One shared pill DNA, mobile-aware popovers.
In the preview: hover a pill to open its menu (tap on mobile), toggle Audio, type into Seconds and press Enter.
npx shadcn@latest add @visualsandbox-ui/composer-pillsRequires the one-time registry entry from Installation — or use the full URL with zero setup:
npx shadcn@latest add https://ui.visualsandbox.com/r/composer-pills.jsonDesigned for the Composer's controls row, but each pill stands alone.
import {
ActionPopover,
NumberPill,
PillSelect,
TogglePill,
} from "@/components/visualsandbox-ui/composer-pills";
<PillSelect value={ratio} options={["1:1", "16:9", "9:16"]} onChange={setRatio} />
<TogglePill label="Audio" on={audio} onChange={setAudio} />
<NumberPill label="Seconds" value={s} min={4} max={16} integer onChange={setS} />
<ActionPopover
icon={<Plus className="h-4 w-4" />}
ariaLabel="Add reference"
onClick={openPicker}
items={[{ icon: <Upload size={14} />, label: "Upload", onClick: openPicker }]}
/>PillSelect and ActionPopover open on hover (Radix HoverCard, 50ms in / 150ms out). The trigger's primary onClick still fires without opening the menu.< 768px): hover doesn't exist, so both switch to a tap-to-open Radix Popover that closes on selection.min/max and commits on blur or Enter — half-typed values never reach onChange.| Prop | Type | Default | Notes |
|---|---|---|---|
value | T extends string | — | |
options | readonly T[] | — | |
onChange | (v: T) => void | — | |
labelFor | (v: T) => ReactNode | — | Display label per option. |
className | string | — | Re-skin the trigger pill. |
contentClassName | string | — | Re-skin the floating panel. |
itemClassName | string | — | Re-skin the option rows. |
| Prop | Type | Default | Notes |
|---|---|---|---|
label | string | — | Shows "{label} On" when on. |
on | boolean | — | |
onChange | (next: boolean) => void | — | |
className | string | — | Re-skin the off state. |
onClassName | string | — | Re-skin the on state (filled default). |
| Prop | Type | Default | Notes |
|---|---|---|---|
label | string | — | |
value | number | — | |
min / max | number | null | null | Clamped on commit. |
integer | boolean | false | Rounds on commit, numeric keyboard. |
onChange | (next: number) => void | — | Fires on blur/Enter only. |
className | string | — | Re-skin the pill. |
inputClassName | string | — | Re-skin the inner input. |
| Prop | Type | Default | Notes |
|---|---|---|---|
icon | ReactNode | — | Trigger icon. |
ariaLabel | string | — | |
onClick | () => void | — | Primary action on the trigger itself. |
loading | boolean | false | Swaps the icon for a spinner. |
disabled | boolean | false | |
items | ActionPopoverItem[] | — | Empty array → plain button, no popover. |
openDelay / closeDelay | number | 50/150 | Hover intent timing (desktop). |
className / contentClassName / itemClassName | string | — | Re-skin trigger / panel / rows. |