import { Fragment, type ComponentProps, useMemo, useState } from 'react' import { type VariantProps, cva } from 'class-variance-authority' import { Track } from 'livekit-client' import { MicIcon, MicOffIcon, MonitorUpIcon, MonitorOffIcon, LoaderIcon, VideoIcon, VideoOffIcon } from 'lucide-react' import { Toggle } from '@/components/ui/toggle' import { cn } from '@/lib/utils' export const agentTrackToggleVariants = cva(['size-9'], { variants: { size: { default: 'h-9 min-w-9 px-2', sm: 'h-8 min-w-8 px-1.5', lg: 'h-10 min-w-10 px-2.5', }, variant: { default: [ 'data-[state=off]:bg-destructive/10 data-[state=off]:text-destructive', 'data-[state=off]:hover:bg-destructive/15', 'data-[state=off]:focus-visible:ring-destructive/30', 'data-[state=on]:bg-accent data-[state=on]:text-accent-foreground', 'data-[state=on]:hover:bg-foreground/10', ], outline: [ 'data-[state=off]:border-destructive/20 data-[state=off]:bg-destructive/10 data-[state=off]:text-destructive', 'data-[state=off]:hover:bg-destructive/15 data-[state=off]:hover:text-destructive', 'data-[state=off]:focus:text-destructive', 'data-[state=off]:focus-visible:border-destructive data-[state=off]:focus-visible:ring-destructive/30', 'data-[state=on]:hover:border-foreground/12 data-[state=on]:hover:bg-foreground/10', 'dark:data-[state=on]:hover:bg-foreground/10', ], }, }, defaultVariants: { variant: 'default', }, }) function getSourceIcon(source: Track.Source, enabled: boolean, pending = false) { if (pending) { return LoaderIcon } switch (source) { case Track.Source.Microphone: return enabled ? MicIcon : MicOffIcon case Track.Source.Camera: return enabled ? VideoIcon : VideoOffIcon case Track.Source.ScreenShare: return enabled ? MonitorUpIcon : MonitorOffIcon default: return Fragment } } /** * Props for the AgentTrackToggle component. */ export type AgentTrackToggleProps = VariantProps & ComponentProps<'button'> & { /** * The size of the toggle. */ size?: 'sm' | 'default' | 'lg' /** * The variant of the toggle. * @defaultValue 'default' */ variant?: 'default' | 'outline' /** * The track source to toggle (Microphone, Camera, or ScreenShare). */ source: 'camera' | 'microphone' | 'screen_share' /** * Whether the toggle is in a pending/loading state. * When true, displays a loading spinner icon. * @defaultValue false */ pending?: boolean /** * Whether the toggle is currently pressed/enabled. * @defaultValue false */ pressed?: boolean /** * The default pressed state when uncontrolled. * @defaultValue false */ defaultPressed?: boolean /** * Callback fired when the pressed state changes. */ onPressedChange?: (pressed: boolean) => void } /** * A toggle button for controlling track publishing state. * Displays appropriate icons based on the track source and state. * * @extends ComponentProps<'button'> * * @example * ```tsx * setMicEnabled(pressed)} * /> * ``` */ export function AgentTrackToggle({ size = 'default', variant = 'default', source, pending = false, pressed, defaultPressed = false, className, onPressedChange, ...props }: AgentTrackToggleProps) { const [uncontrolledPressed, setUncontrolledPressed] = useState(defaultPressed ?? false) const isControlled = pressed !== undefined const resolvedPressed = useMemo( () => (isControlled ? pressed : uncontrolledPressed) ?? false, [isControlled, pressed, uncontrolledPressed] ) const IconComponent = getSourceIcon(source as Track.Source, resolvedPressed, pending) const handlePressedChange = (nextPressed: boolean) => { if (!isControlled) { setUncontrolledPressed(nextPressed) } onPressedChange?.(nextPressed) } return ( {props.children} ) }