Support Chat Widget
The corner launcher, with real availability instead of a stock reassurance, article deflection before the form, no self-opening, and a way to make it go away.
The call grid with the state that makes it usable - active speaker, per-tile connection quality, hide self-view, and the you-are-muted nudge.
components/video-call-layout.tsxnpx hoverlab add video-call-layout
Or over MCP, from your editor's agent — no account needed.
Free to read, copy and install, for personal and non-commercial projects. Shipping it in client work or a paid product needs Pro ($79 once). The source lands in your repo and stops being ours — no attribution, nothing to upgrade.
Start a page with this section — add more, order them, and leave with the page source.
18:42 · 5 people
Priya Raman is speaking
Your microphone is off — we can see you talking.
Rendered live in your current theme — this is the same component whose source is below, not a screenshot of it.
'use client'
/**
* <VideoCallLayout> — the call screen, with the bits that get skipped.
*
* A grid of rectangles is the easy 20% of a call UI. The rest is state, and it
* is the state that decides whether a call is usable:
*
* - **Who is speaking.** An active-speaker ring, not just a name label. In a
* grid of six, working out who is talking from lip movement over a lagged
* stream is genuinely hard, and the ring is the fix.
* - **Who is muted, and who is muted *and* talking.** The second one is the
* most common event on any call and deserves its own affordance — a "your
* microphone is off" nudge, which this shows when you speak while muted.
* - **Connection quality per tile.** A frozen participant is either frozen or
* gone, and the difference matters. Two bars beside the name says which.
* - **A self-view that can be hidden.** Constantly seeing yourself is
* measurably tiring, and "hide self-view" is the single most appreciated
* control any conferencing product has shipped.
*
* THE GRID IS `auto-fit`, NOT A BREAKPOINT TABLE
*
* `repeat(auto-fit, minmax(…))` with a fixed aspect ratio per tile gives the
* right number of columns at every width without a media query per count, and
* it degrades to one column on a phone by itself. Hand-written per-count grid
* classes are how a call layout ends up with a special case for five people.
*
* THE CONTROL BAR IS ANCHORED AND FINGER-SIZED. 44px minimum targets, the
* destructive Leave separated from the toggles by a gap rather than by colour
* alone, because "I meant to mute" is the worst possible misclick.
*
* ACCESSIBILITY: each toggle's state is in its accessible name ("Microphone,
* off") rather than only in a slashed glyph; the speaking indicator is mirrored
* in a visually hidden live region, throttled to changes rather than to frames;
* tiles are list items with real names, not decorative boxes.
*/
import * as React from 'react'
import {
Hand,
Mic,
MicOff,
Monitor,
PhoneOff,
Signal,
SignalLow,
Users,
Video,
VideoOff,
} from 'lucide-react'
export interface Participant {
id: string
name: string
initials: string
muted?: boolean
cameraOff?: boolean
speaking?: boolean
handRaised?: boolean
quality: 'good' | 'poor'
self?: boolean
}
export interface VideoCallLayoutProps {
title?: string
elapsed?: string
participants?: Participant[]
className?: string
}
const DEFAULT_PARTICIPANTS: Participant[] = [
{ id: 'p1', name: 'Priya Raman', initials: 'PR', speaking: true, quality: 'good' },
{ id: 'p2', name: 'Sam Okafor', initials: 'SO', muted: true, quality: 'good' },
{ id: 'p3', name: 'Jordan Lee', initials: 'JL', cameraOff: true, quality: 'poor' },
{ id: 'p4', name: 'Marte Haugen', initials: 'MH', handRaised: true, muted: true, quality: 'good' },
{ id: 'p5', name: 'You', initials: 'YU', self: true, quality: 'good' },
]
export function VideoCallLayout({
title = 'Rotterdam delivery review',
elapsed = '18:42',
participants = DEFAULT_PARTICIPANTS,
className = '',
}: VideoCallLayoutProps) {
const [micOn, setMicOn] = React.useState(false)
const [cameraOn, setCameraOn] = React.useState(true)
const [sharing, setSharing] = React.useState(false)
const [hideSelf, setHideSelf] = React.useState(false)
const [speakingWhileMuted, setSpeakingWhileMuted] = React.useState(true)
const speaker = participants.find((p) => p.speaking)
const visible = participants.filter((p) => !(p.self && hideSelf))
return (
<section className={`bg-background px-4 py-10 sm:px-6 ${className}`}>
<div className="mx-auto max-w-4xl overflow-hidden rounded-2xl border border-border bg-foreground/[0.04] dark:bg-foreground/[0.06]">
<header className="flex flex-wrap items-center justify-between gap-3 border-b border-border p-3">
<div>
<h2 className="text-sm font-semibold">{title}</h2>
<p className="text-xs text-muted-foreground">
{elapsed} · {participants.length} people
</p>
</div>
<button
type="button"
className="inline-flex items-center gap-1.5 rounded-lg border border-border bg-card px-2.5 py-1.5 text-xs font-medium transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
<Users aria-hidden className="h-3.5 w-3.5" />
People
</button>
</header>
{/* auto-fit, not a breakpoint table — see the docblock. */}
<ul
className="grid gap-2 p-3"
style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(11rem, 1fr))' }}
>
{visible.map((person) => (
<li
key={person.id}
className={`relative overflow-hidden rounded-xl bg-foreground/10 ring-2 transition-colors ${
person.speaking ? 'ring-primary' : 'ring-transparent'
}`}
style={{ aspectRatio: '4 / 3' }}
>
<div className="grid h-full place-items-center">
{person.cameraOff ? (
<span className="grid h-12 w-12 place-items-center rounded-full bg-card text-sm font-bold text-muted-foreground">
{person.initials}
</span>
) : (
<span
aria-hidden
className="grid h-12 w-12 place-items-center rounded-full bg-primary/20 text-sm font-bold text-primary"
>
{person.initials}
</span>
)}
</div>
{person.handRaised ? (
<span className="absolute top-2 end-2 grid h-6 w-6 place-items-center rounded-full bg-amber-500 text-white">
<Hand aria-hidden className="h-3.5 w-3.5" />
<span className="sr-only">{person.name} has a hand raised</span>
</span>
) : null}
<div className="absolute inset-x-2 bottom-2 flex items-center gap-1.5 rounded-lg bg-background/80 px-2 py-1 backdrop-blur">
{person.muted ? (
<MicOff aria-hidden className="h-3 w-3 shrink-0 text-destructive" />
) : (
<Mic aria-hidden className="h-3 w-3 shrink-0 text-muted-foreground" />
)}
<span className="min-w-0 flex-1 break-words text-[11px] font-medium">
{person.name}
{person.self ? ' (you)' : ''}
</span>
{/* Frozen or gone — the difference matters. */}
{person.quality === 'poor' ? (
<span className="ms-auto inline-flex items-center gap-1 text-[10px] text-amber-600 dark:text-amber-400">
<SignalLow aria-hidden className="h-3 w-3" />
Weak
</span>
) : (
<Signal aria-hidden className="ms-auto h-3 w-3 text-muted-foreground" />
)}
<span className="sr-only">
{person.muted ? 'Muted. ' : ''}
{person.cameraOff ? 'Camera off. ' : ''}
{person.quality === 'poor' ? 'Weak connection.' : ''}
</span>
</div>
</li>
))}
</ul>
{/* Changes only, not frames. */}
<p aria-live="polite" className="sr-only">
{speaker ? `${speaker.name} is speaking` : 'Nobody is speaking'}
</p>
{/* The most common event on any call. */}
{!micOn && speakingWhileMuted ? (
<div
role="status"
className="mx-3 mb-2 flex flex-wrap items-center justify-between gap-3 rounded-xl border border-amber-500/40 bg-amber-500/10 px-3 py-2"
>
<p className="text-xs font-medium text-amber-800 dark:text-amber-300">
Your microphone is off — we can see you talking.
</p>
<div className="flex gap-2">
<button
type="button"
onClick={() => {
setMicOn(true)
setSpeakingWhileMuted(false)
}}
className="rounded-lg bg-amber-600 px-2.5 py-1 text-xs font-semibold text-white transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
Unmute
</button>
<button
type="button"
onClick={() => setSpeakingWhileMuted(false)}
className="rounded-lg px-2 py-1 text-xs font-medium text-amber-800 transition-colors hover:bg-amber-500/10 dark:text-amber-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
Dismiss
</button>
</div>
</div>
) : null}
<div className="flex flex-wrap items-center justify-center gap-2 border-t border-border p-3">
{(
[
{
on: micOn,
set: () => setMicOn((v) => !v),
onIcon: Mic,
offIcon: MicOff,
label: 'Microphone',
},
{
on: cameraOn,
set: () => setCameraOn((v) => !v),
onIcon: Video,
offIcon: VideoOff,
label: 'Camera',
},
{
on: sharing,
set: () => setSharing((v) => !v),
onIcon: Monitor,
offIcon: Monitor,
label: 'Screen share',
},
] as const
).map((control) => {
const Icon = control.on ? control.onIcon : control.offIcon
return (
<button
key={control.label}
type="button"
onClick={control.set}
aria-pressed={control.on}
// 44px minimum — this bar is used with a thumb.
className={`inline-flex h-11 min-w-11 items-center justify-center gap-2 rounded-xl px-3 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${
control.on
? 'bg-card text-foreground hover:bg-muted'
: 'bg-destructive/10 text-destructive hover:bg-destructive/20'
}`}
>
<Icon aria-hidden className="h-4 w-4" />
{/* State in the name, not only in a slashed glyph. */}
<span className="sr-only">
{control.label}, {control.on ? 'on' : 'off'}
</span>
</button>
)
})}
<button
type="button"
onClick={() => setHideSelf((v) => !v)}
aria-pressed={hideSelf}
className="inline-flex h-11 items-center gap-2 rounded-xl bg-card px-3 text-xs font-medium transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
{hideSelf ? 'Show self-view' : 'Hide self-view'}
</button>
{/* Separated by a gap, not only by colour. */}
<span aria-hidden className="w-6" />
<button
type="button"
className="inline-flex h-11 items-center gap-2 rounded-xl bg-destructive px-4 text-sm font-semibold text-destructive-foreground transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
>
<PhoneOff aria-hidden className="h-4 w-4" />
Leave
</button>
</div>
</div>
</section>
)
}
bg-card, text-muted-foreground) — it inherits your theme instead of overriding it.Drop it at components/video-call-layout.tsx and import it where you need the section:
import { VideoCallLayout } from '@/components/video-call-layout'2 of this block’s props are simple enough to drive from here. Change them and the block below re-renders — it is the same component whose source is above, not a mock of it. Everything else it accepts is in the table underneath.
Read out of the component’s own type and signature, so this cannot drift from the source below. Every prop has a default — the component renders standalone before you pass it anything.
| Prop | Type | Default |
|---|---|---|
title | string | 'Rotterdam delivery review' |
elapsed | string | '18:42' |
participants | Participant[] | DEFAULT_PARTICIPANTS |
className | string | '' |
The same block rendered once to markup, wrapped as a file your framework compiles. Tailwind classes are framework-agnostic, so the design transfers intact — the behaviour does not.
This block is interactive. The markup below is its initial state with the event handlers stripped — you will need to re-wire the behaviour in your framework.
<!--
Video Call Layout — markup from the Hoverlab catalog.
This is the block rendered once to HTML and wrapped as a component
file. It is not a port of the React source: the Tailwind classes carry
the design, which is the part that took the work, and they are the same
in every framework.
This block is interactive in React and the handlers are NOT here.
Buttons, toggles and menus render in their initial state and do
nothing until you wire them up.
-->
<section class="bg-background px-4 py-10 sm:px-6 ">
<div class="mx-auto max-w-4xl overflow-hidden rounded-2xl border border-border bg-foreground/[0.04] dark:bg-foreground/[0.06]">
<header class="flex flex-wrap items-center justify-between gap-3 border-b border-border p-3">
<div>
<h2 class="text-sm font-semibold">Rotterdam delivery review</h2>
<p class="text-xs text-muted-foreground">18:42 · 5 people</p>
</div>
<button type="button" class="inline-flex items-center gap-1.5 rounded-lg border border-border bg-card px-2.5 py-1.5 text-xs font-medium transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-users h-3.5 w-3.5" aria-hidden="true"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"></path><path d="M16 3.128a4 4 0 0 1 0 7.744"></path><path d="M22 21v-2a4 4 0 0 0-3-3.87"></path><circle cx="9" cy="7" r="4"></circle></svg>People</button>
</header>
<ul class="grid gap-2 p-3" style="grid-template-columns:repeat(auto-fit, minmax(11rem, 1fr))">
<li class="relative overflow-hidden rounded-xl bg-foreground/10 ring-2 transition-colors ring-primary" style="aspect-ratio:4 / 3">
<div class="grid h-full place-items-center">
<span aria-hidden="true" class="grid h-12 w-12 place-items-center rounded-full bg-primary/20 text-sm font-bold text-primary">PR</span>
</div>
<div class="absolute inset-x-2 bottom-2 flex items-center gap-1.5 rounded-lg bg-background/80 px-2 py-1 backdrop-blur">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-mic h-3 w-3 shrink-0 text-muted-foreground" aria-hidden="true">
<path d="M12 19v3"></path>
<path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
<rect x="9" y="2" width="6" height="13" rx="3"></rect>
</svg>
<span class="min-w-0 flex-1 break-words text-[11px] font-medium">Priya Raman</span>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-signal ms-auto h-3 w-3 text-muted-foreground" aria-hidden="true">
<path d="M2 20h.01"></path>
<path d="M7 20v-4"></path>
<path d="M12 20v-8"></path>
<path d="M17 20V8"></path>
<path d="M22 4v16"></path>
</svg>
<span class="sr-only"></span>
</div>
</li>
<li class="relative overflow-hidden rounded-xl bg-foreground/10 ring-2 transition-colors ring-transparent" style="aspect-ratio:4 / 3">
<div class="grid h-full place-items-center">
<span aria-hidden="true" class="grid h-12 w-12 place-items-center rounded-full bg-primary/20 text-sm font-bold text-primary">SO</span>
</div>
<div class="absolute inset-x-2 bottom-2 flex items-center gap-1.5 rounded-lg bg-background/80 px-2 py-1 backdrop-blur">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-mic-off h-3 w-3 shrink-0 text-destructive" aria-hidden="true">
<line x1="2" x2="22" y1="2" y2="22"></line>
<path d="M18.89 13.23A7.12 7.12 0 0 0 19 12v-2"></path>
<path d="M5 10v2a7 7 0 0 0 12 5"></path>
<path d="M15 9.34V5a3 3 0 0 0-5.68-1.33"></path>
<path d="M9 9v3a3 3 0 0 0 5.12 2.12"></path>
<line x1="12" x2="12" y1="19" y2="22"></line>
</svg>
<span class="min-w-0 flex-1 break-words text-[11px] font-medium">Sam Okafor</span>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-signal ms-auto h-3 w-3 text-muted-foreground" aria-hidden="true">
<path d="M2 20h.01"></path>
<path d="M7 20v-4"></path>
<path d="M12 20v-8"></path>
<path d="M17 20V8"></path>
<path d="M22 4v16"></path>
</svg>
<span class="sr-only">Muted.</span>
</div>
</li>
<li class="relative overflow-hidden rounded-xl bg-foreground/10 ring-2 transition-colors ring-transparent" style="aspect-ratio:4 / 3">
<div class="grid h-full place-items-center">
<span class="grid h-12 w-12 place-items-center rounded-full bg-card text-sm font-bold text-muted-foreground">JL</span>
</div>
<div class="absolute inset-x-2 bottom-2 flex items-center gap-1.5 rounded-lg bg-background/80 px-2 py-1 backdrop-blur">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-mic h-3 w-3 shrink-0 text-muted-foreground" aria-hidden="true">
<path d="M12 19v3"></path>
<path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
<rect x="9" y="2" width="6" height="13" rx="3"></rect>
</svg>
<span class="min-w-0 flex-1 break-words text-[11px] font-medium">Jordan Lee</span>
<span class="ms-auto inline-flex items-center gap-1 text-[10px] text-amber-600 dark:text-amber-400"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-signal-low h-3 w-3" aria-hidden="true"><path d="M2 20h.01"></path><path d="M7 20v-4"></path></svg>Weak</span>
<span class="sr-only">Camera off. Weak connection.</span>
</div>
</li>
<li class="relative overflow-hidden rounded-xl bg-foreground/10 ring-2 transition-colors ring-transparent" style="aspect-ratio:4 / 3">
<div class="grid h-full place-items-center">
<span aria-hidden="true" class="grid h-12 w-12 place-items-center rounded-full bg-primary/20 text-sm font-bold text-primary">MH</span>
</div>
<span class="absolute top-2 end-2 grid h-6 w-6 place-items-center rounded-full bg-amber-500 text-white">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-hand h-3.5 w-3.5" aria-hidden="true">
<path d="M18 11V6a2 2 0 0 0-2-2a2 2 0 0 0-2 2"></path>
<path d="M14 10V4a2 2 0 0 0-2-2a2 2 0 0 0-2 2v2"></path>
<path d="M10 10.5V6a2 2 0 0 0-2-2a2 2 0 0 0-2 2v8"></path>
<path d="M18 8a2 2 0 1 1 4 0v6a8 8 0 0 1-8 8h-2c-2.8 0-4.5-.86-5.99-2.34l-3.6-3.6a2 2 0 0 1 2.83-2.82L7 15"></path>
</svg>
<span class="sr-only">Marte Haugen has a hand raised</span>
</span>
<div class="absolute inset-x-2 bottom-2 flex items-center gap-1.5 rounded-lg bg-background/80 px-2 py-1 backdrop-blur">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-mic-off h-3 w-3 shrink-0 text-destructive" aria-hidden="true">
<line x1="2" x2="22" y1="2" y2="22"></line>
<path d="M18.89 13.23A7.12 7.12 0 0 0 19 12v-2"></path>
<path d="M5 10v2a7 7 0 0 0 12 5"></path>
<path d="M15 9.34V5a3 3 0 0 0-5.68-1.33"></path>
<path d="M9 9v3a3 3 0 0 0 5.12 2.12"></path>
<line x1="12" x2="12" y1="19" y2="22"></line>
</svg>
<span class="min-w-0 flex-1 break-words text-[11px] font-medium">Marte Haugen</span>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-signal ms-auto h-3 w-3 text-muted-foreground" aria-hidden="true">
<path d="M2 20h.01"></path>
<path d="M7 20v-4"></path>
<path d="M12 20v-8"></path>
<path d="M17 20V8"></path>
<path d="M22 4v16"></path>
</svg>
<span class="sr-only">Muted.</span>
</div>
</li>
<li class="relative overflow-hidden rounded-xl bg-foreground/10 ring-2 transition-colors ring-transparent" style="aspect-ratio:4 / 3">
<div class="grid h-full place-items-center">
<span aria-hidden="true" class="grid h-12 w-12 place-items-center rounded-full bg-primary/20 text-sm font-bold text-primary">YU</span>
</div>
<div class="absolute inset-x-2 bottom-2 flex items-center gap-1.5 rounded-lg bg-background/80 px-2 py-1 backdrop-blur">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-mic h-3 w-3 shrink-0 text-muted-foreground" aria-hidden="true">
<path d="M12 19v3"></path>
<path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
<rect x="9" y="2" width="6" height="13" rx="3"></rect>
</svg>
<span class="min-w-0 flex-1 break-words text-[11px] font-medium">You (you)</span>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-signal ms-auto h-3 w-3 text-muted-foreground" aria-hidden="true">
<path d="M2 20h.01"></path>
<path d="M7 20v-4"></path>
<path d="M12 20v-8"></path>
<path d="M17 20V8"></path>
<path d="M22 4v16"></path>
</svg>
<span class="sr-only"></span>
</div>
</li>
</ul>
<p aria-live="polite" class="sr-only">Priya Raman is speaking</p>
<div role="status" class="mx-3 mb-2 flex flex-wrap items-center justify-between gap-3 rounded-xl border border-amber-500/40 bg-amber-500/10 px-3 py-2">
<p class="text-xs font-medium text-amber-800 dark:text-amber-300">Your microphone is off — we can see you talking.</p>
<div class="flex gap-2">
<button type="button" class="rounded-lg bg-amber-600 px-2.5 py-1 text-xs font-semibold text-white transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">Unmute</button>
<button type="button" class="rounded-lg px-2 py-1 text-xs font-medium text-amber-800 transition-colors hover:bg-amber-500/10 dark:text-amber-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">Dismiss</button>
</div>
</div>
<div class="flex flex-wrap items-center justify-center gap-2 border-t border-border p-3">
<button type="button" aria-pressed="false" class="inline-flex h-11 min-w-11 items-center justify-center gap-2 rounded-xl px-3 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring bg-destructive/10 text-destructive hover:bg-destructive/20">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-mic-off h-4 w-4" aria-hidden="true">
<line x1="2" x2="22" y1="2" y2="22"></line>
<path d="M18.89 13.23A7.12 7.12 0 0 0 19 12v-2"></path>
<path d="M5 10v2a7 7 0 0 0 12 5"></path>
<path d="M15 9.34V5a3 3 0 0 0-5.68-1.33"></path>
<path d="M9 9v3a3 3 0 0 0 5.12 2.12"></path>
<line x1="12" x2="12" y1="19" y2="22"></line>
</svg>
<span class="sr-only">Microphone, off</span>
</button>
<button type="button" aria-pressed="true" class="inline-flex h-11 min-w-11 items-center justify-center gap-2 rounded-xl px-3 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring bg-card text-foreground hover:bg-muted">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-video h-4 w-4" aria-hidden="true">
<path d="m16 13 5.223 3.482a.5.5 0 0 0 .777-.416V7.87a.5.5 0 0 0-.752-.432L16 10.5"></path>
<rect x="2" y="6" width="14" height="12" rx="2"></rect>
</svg>
<span class="sr-only">Camera, on</span>
</button>
<button type="button" aria-pressed="false" class="inline-flex h-11 min-w-11 items-center justify-center gap-2 rounded-xl px-3 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring bg-destructive/10 text-destructive hover:bg-destructive/20">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-monitor h-4 w-4" aria-hidden="true">
<rect width="20" height="14" x="2" y="3" rx="2"></rect>
<line x1="8" x2="16" y1="21" y2="21"></line>
<line x1="12" x2="12" y1="17" y2="21"></line>
</svg>
<span class="sr-only">Screen share, off</span>
</button>
<button type="button" aria-pressed="false" class="inline-flex h-11 items-center gap-2 rounded-xl bg-card px-3 text-xs font-medium transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">Hide self-view</button>
<span aria-hidden="true" class="w-6"></span>
<button type="button" class="inline-flex h-11 items-center gap-2 rounded-xl bg-destructive px-4 text-sm font-semibold text-destructive-foreground transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-phone-off h-4 w-4" aria-hidden="true"><path d="M10.1 13.9a14 14 0 0 0 3.732 2.668 1 1 0 0 0 1.213-.303l.355-.465A2 2 0 0 1 17 15h3a2 2 0 0 1 2 2v3a2 2 0 0 1-2 2 18 18 0 0 1-12.728-5.272"></path><path d="M22 2 2 22"></path><path d="M4.76 13.582A18 18 0 0 1 2 4a2 2 0 0 1 2-2h3a2 2 0 0 1 2 2v3a2 2 0 0 1-.8 1.6l-.468.351a1 1 0 0 0-.292 1.233 14 14 0 0 0 .244.473"></path></svg>Leave</button>
</div>
</div>
</section>
What each framework gets across the whole catalog — effects convert properly; this rung is markup.
The component, its props, the design tokens it expects and the command that installs it — as one prompt. Paste it into Claude, Cursor, v0 or ChatGPT and what they build around it will match the rest of the catalog instead of inventing its own system.
All of them free to read, copy and install — no account, no locked tiles, no watermarked preview. The whole catalog is open, and so are the API and the CLI.
Browse CommunicationCopying the code is free. Putting it in client work or a paid product is what Pro is for — the licence, not the access.
The corner launcher, with real availability instead of a stock reassurance, article deflection before the form, no self-opening, and a way to make it go away.
Bubbles with the parts that get skipped: per-message delivery state, grouping by author and time, day separators, and a retry on the failed bubble itself.
Five kinds of thing that is not text - image, document, link, location and an upload still in flight - each answering the question its own kind raises.
The list half of a two-pane inbox, with unread and selected as visibly different states and bulk selection as a mode rather than a hover checkbox.
The reading pane with the composer always present, quoted history collapsed behind a count, and an internal-note mode whose send button names the recipient.
A voice note you can navigate: a waveform that is a real seek control, a speed button, a transcript, and a played state that survives finishing.
The green room, where every picker has a test beside it - an input meter, a test tone, a camera preview - and blocked permission is a state, not an error.
The customer side of a ticket, where a named owner and a deadline replace Status: Open - including the waiting-on-you state that stops the argument about response times.