{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"agent-track-control","title":"Agent Track Control","description":"A control for managing media tracks (microphone, camera, screen share) with appropriate icons and loading states.","dependencies":["livekit-client@^2.0.0","@livekit/components-react@^2.0.0","class-variance-authority","lucide-react"],"registryDependencies":["select","@agents-ui/agent-track-toggle","utils"],"files":[{"path":"components/agents-ui/agent-track-control.tsx","content":"'use client';\n\nimport { useEffect, useMemo, useState } from 'react';\nimport { type VariantProps, cva } from 'class-variance-authority';\nimport {\n  type TrackReferenceOrPlaceholder,\n  useMaybeRoomContext,\n  useMediaDeviceSelect,\n} from '@livekit/components-react';\nimport { AgentAudioVisualizerBar } from '@/components/agents-ui/agent-audio-visualizer-bar';\nimport { AgentTrackToggle } from '@/components/agents-ui/agent-track-toggle';\nimport {\n  Select,\n  SelectContent,\n  SelectItem,\n  SelectTrigger,\n  SelectValue,\n} from '@/components/ui/select';\nimport { type toggleVariants } from '@/components/ui/toggle';\nimport { cn } from '@/lib/utils';\n\nconst selectVariants = cva(\n  [\n    'rounded-l-none shadow-none pl-2 ',\n    'text-foreground hover:text-muted-foreground',\n    'peer-data-[state=on]/track:bg-muted peer-data-[state=on]/track:hover:bg-foreground/10',\n    'peer-data-[state=off]/track:text-destructive',\n    'peer-data-[state=off]/track:focus-visible:border-destructive peer-data-[state=off]/track:focus-visible:ring-destructive/30',\n    '[&_svg]:opacity-100',\n  ],\n  {\n    variants: {\n      variant: {\n        default: [\n          'border-none',\n          'peer-data-[state=off]/track:bg-destructive/10',\n          'peer-data-[state=off]/track:hover:bg-destructive/15',\n          'peer-data-[state=off]/track:[&_svg]:text-destructive!',\n\n          'dark:peer-data-[state=on]/track:bg-accent',\n          'dark:peer-data-[state=on]/track:hover:bg-foreground/10',\n          'dark:peer-data-[state=off]/track:bg-destructive/10',\n          'dark:peer-data-[state=off]/track:hover:bg-destructive/15',\n        ],\n        outline: [\n          'border border-l-0',\n          'peer-data-[state=off]/track:border-destructive/20',\n          'peer-data-[state=off]/track:bg-destructive/10',\n          'peer-data-[state=off]/track:hover:bg-destructive/15',\n          'peer-data-[state=off]/track:[&_svg]:text-destructive!',\n          'peer-data-[state=on]/track:hover:border-foreground/12',\n\n          'dark:peer-data-[state=off]/track:bg-destructive/10',\n          'dark:peer-data-[state=off]/track:hover:bg-destructive/15',\n          'dark:peer-data-[state=on]/track:bg-accent',\n          'dark:peer-data-[state=on]/track:hover:bg-foreground/10',\n        ],\n      },\n      size: {\n        default: 'w-[180px]',\n        sm: 'w-auto',\n      },\n    },\n    defaultVariants: {\n      variant: 'default',\n      size: 'default',\n    },\n  },\n);\n\n/**\n * Props for the TrackDeviceSelect component. */\ntype TrackDeviceSelectProps = React.ComponentProps<typeof SelectTrigger> &\n  VariantProps<typeof selectVariants> & {\n    /**\n     * The type of media device (audioinput or videoinput).\n     */\n    kind: MediaDeviceKind;\n    /**\n     * Array of available devices\n     */\n    devices: MediaDeviceInfo[];\n    /**\n     * Active device ID\n     */\n    activeDeviceId?: string;\n    /**\n     * The size of the select.\n     * @defaultValue 'default'\n     */\n    size?: 'default' | 'sm';\n    /**\n     * The variant of the select.\n     * @defaultValue 'default'\n     */\n    variant?: 'default' | 'outline' | null;\n    /**\n     * Whether to request permissions for the media device.\n     */\n    requestPermissions?: boolean;\n    /**\n     * Callback when the select is opened or closed.\n     */\n    onOpen: (open: boolean) => void;\n    /**\n     * Callback when the active device changes.\n     */\n    onActiveDeviceChange?: (deviceId: string) => void;\n  };\n\n/**\n * A select component for selecting a media device.\n *\n * @extends ComponentProps<'button'>\n *\n * @example\n * ```tsx\n * <TrackDeviceSelect\n *   size=\"sm\"\n *   variant=\"outline\"\n *   kind=\"audioinput\"\n *   devices={devices}\n *   activeDeviceId={activeDeviceId}\n *   onOpen={setOpen}\n *   onActiveDeviceChange={setActiveDeviceId}\n * />\n * ```\n */\nfunction TrackDeviceSelect({\n  kind,\n  devices,\n  activeDeviceId,\n  size = 'default',\n  variant = 'default',\n  className,\n  onOpen,\n  onActiveDeviceChange,\n  ...props\n}: TrackDeviceSelectProps) {\n  const [open, setOpen] = useState(false);\n\n  const handleOpenChange = (open: boolean) => {\n    setOpen(open);\n    onOpen(open);\n  };\n\n  if (devices.length < 2) {\n    return null;\n  }\n\n  return (\n    <Select\n      open={open}\n      value={activeDeviceId}\n      onOpenChange={handleOpenChange}\n      onValueChange={onActiveDeviceChange}\n    >\n      <SelectTrigger className={cn(selectVariants({ size, variant }), className)} {...props}>\n        {size !== 'sm' && (\n          <SelectValue className=\"font-mono text-sm\" placeholder={`Select a ${kind}`} />\n        )}\n      </SelectTrigger>\n      <SelectContent position=\"popper\">\n        {devices.map((device) => (\n          <SelectItem key={device.deviceId} value={device.deviceId} className=\"font-mono text-xs\">\n            {device.label}\n          </SelectItem>\n        ))}\n      </SelectContent>\n    </Select>\n  );\n}\n\n/**\n * Props for the AgentTrackControl component.\n */\nexport type AgentTrackControlProps = VariantProps<typeof toggleVariants> & {\n  /**\n   * The type of media device (audioinput or videoinput).\n   */\n  kind: MediaDeviceKind;\n  /**\n   * The track source to control (Microphone, Camera, or ScreenShare).\n   */\n  source: 'camera' | 'microphone' | 'screen_share';\n  /**\n   * Whether the track is currently enabled/published.\n   */\n  pressed?: boolean;\n  /**\n   * Whether the control is in a pending/loading state.\n   */\n  pending?: boolean;\n  /**\n   * Whether the control is disabled.\n   */\n  disabled?: boolean;\n  /**\n   * The audio track reference for visualization (only for microphone).\n   */\n  audioTrack?: TrackReferenceOrPlaceholder;\n  /**\n   * Additional CSS class names to apply to the container.\n   */\n  className?: string;\n  /**\n   * Callback when the pressed state changes.\n   */\n  onPressedChange?: (pressed: boolean) => void;\n  /**\n   * Callback when a media device error occurs.\n   */\n  onMediaDeviceError?: (error: Error) => void;\n  /**\n   * Callback when the active device changes.\n   */\n  onActiveDeviceChange?: (deviceId: string) => void;\n};\n\n/**\n * A combined track toggle and device selector control.\n * Includes a toggle button and a dropdown to select the active device.\n * For microphone tracks, displays an audio visualizer.\n *\n * @example\n * ```tsx\n * <AgentTrackControl\n *   kind=\"audioinput\"\n *   source={Track.Source.Microphone}\n *   pressed={isMicEnabled}\n *   audioTrack={micTrackRef}\n *   onPressedChange={(pressed) => setMicEnabled(pressed)}\n *   onActiveDeviceChange={(deviceId) => setMicDevice(deviceId)}\n * />\n * ```\n */\nexport function AgentTrackControl({\n  kind,\n  variant = 'default',\n  source,\n  pressed,\n  pending,\n  disabled,\n  className,\n  audioTrack,\n  onPressedChange,\n  onMediaDeviceError,\n  onActiveDeviceChange,\n}: AgentTrackControlProps) {\n  const room = useMaybeRoomContext();\n  const [requestPermissionsState, setRequestPermissionsState] = useState(false);\n  const { devices, activeDeviceId, setActiveMediaDevice } = useMediaDeviceSelect({\n    room,\n    kind,\n    requestPermissions: requestPermissionsState,\n    onError: onMediaDeviceError,\n  });\n\n  useEffect(() => {\n    // A track was already acquired elsewhere (e.g. session.start()), so permission is already\n    // granted — safe to fetch labeled devices without a second getUserMedia prompt. Without\n    // this, the list stays label-less until a page reload, since the device observer only\n    // re-fetches when `requestPermissions` changes value. Screen share has no `kind` and no\n    // associated device list, so it should never trigger a mic/camera permission request.\n    if (pressed && kind) {\n      setRequestPermissionsState(true);\n    }\n  }, [pressed, kind]);\n\n  const handleOpenChange = (open: boolean) => {\n    if (open) {\n      setRequestPermissionsState(true);\n    }\n  };\n\n  const handleActiveDeviceChange = (deviceId: string) => {\n    setActiveMediaDevice(deviceId);\n    onActiveDeviceChange?.(deviceId);\n  };\n\n  const filteredDevices = useMemo(() => devices.filter((d) => d.deviceId !== ''), [devices]);\n  // Before permission is granted, the browser reports devices with blank ids, which get filtered\n  // out here — that's an *unknown* device count, not a confirmed empty one. Only treat it as\n  // \"no devices\" once a permission-gated check has actually run and still come up empty;\n  // otherwise the toggle disables itself before the user ever gets a chance to grant permission.\n  const noDevices = Boolean(kind) && requestPermissionsState && filteredDevices.length === 0;\n  const resolvedPressed = pressed && !noDevices;\n\n  return (\n    <div\n      className={cn(\n        'flex items-center gap-0 rounded-md',\n        variant === 'outline' && 'shadow-xs [&_button]:shadow-none',\n        className,\n      )}\n    >\n      <AgentTrackToggle\n        source={source}\n        pending={pending}\n        variant={variant ?? 'default'}\n        pressed={resolvedPressed}\n        disabled={noDevices || disabled}\n        onPressedChange={onPressedChange}\n        className=\"peer/track group/track focus:z-10 has-[.audiovisualizer]:w-auto has-[.audiovisualizer]:px-3 has-[~_button]:rounded-r-none has-[~_button]:border-r-0 has-[~_button]:pr-2 has-[~_button]:pl-3\"\n      >\n        {audioTrack && (\n          <AgentAudioVisualizerBar\n            size=\"icon\"\n            barCount={3}\n            state={resolvedPressed ? 'speaking' : 'disconnected'}\n            audioTrack={resolvedPressed ? audioTrack : undefined}\n            className=\"audiovisualizer flex h-6 w-auto items-center justify-center gap-0.5\"\n          >\n            <span\n              className={cn([\n                'h-full min-h-0.5 w-0.5 origin-center',\n                'group-data-[state=on]/track:bg-foreground group-data-[state=off]/track:bg-destructive',\n                'data-lk-muted:bg-muted',\n              ])}\n            />\n          </AgentAudioVisualizerBar>\n        )}\n      </AgentTrackToggle>\n      {kind && (\n        <TrackDeviceSelect\n          size=\"sm\"\n          kind={kind}\n          variant={variant}\n          devices={filteredDevices}\n          activeDeviceId={activeDeviceId}\n          onOpen={handleOpenChange}\n          onActiveDeviceChange={handleActiveDeviceChange}\n          className={cn([\n            'relative',\n            'before:bg-border before:absolute before:inset-y-0 before:left-0 before:my-2.5 before:w-px has-[~_button]:before:content-[\"\"]',\n            !resolvedPressed && 'before:bg-destructive/20',\n          ])}\n        />\n      )}\n    </div>\n  );\n}\n","type":"registry:component"}],"type":"registry:component"}