1"use client"
2
3import { useEffect, useId, useRef, useState } from "react"
4import { CaretDown, Check, VideoCamera } from "@phosphor-icons/react"
5
6import { Button } from "@/components/ui/button"
7import { cn } from "@/lib/utils"
8
9/* FR-08 (P1): pilih kamera bila perangkat punya lebih dari satu.
10 Pola disclosure sederhana: tombol kepala panel + daftar tombol perangkat,
11 jadi seluruh pemilih bisa dipakai keyboard tanpa komponen baru. */
12export function CameraPicker({
13 devices,
14 selectedId,
15 onSelect,
16}: {
17 devices: MediaDeviceInfo[]
18 selectedId: string | null
19 onSelect: (deviceId: string) => void
20}) {
21 const [open, setOpen] = useState(false)
22 const rootRef = useRef<HTMLDivElement>(null)
23 const listId = useId()
24
25 useEffect(() => {
26 if (!open) {
27 return
28 }
29 function handlePointerDown(event: MouseEvent) {
30 if (rootRef.current !== null && !rootRef.current.contains(event.target as Node)) {
31 setOpen(false)
32 }
33 }
34 function handleKeyDown(event: globalThis.KeyboardEvent) {
35 if (event.key === "Escape") {
36 setOpen(false)
37 }
38 }
39 document.addEventListener("mousedown", handlePointerDown)
40 document.addEventListener("keydown", handleKeyDown)
41 return () => {
42 document.removeEventListener("mousedown", handlePointerDown)
43 document.removeEventListener("keydown", handleKeyDown)
44 }
45 }, [open])
46
47 const selectedIndex = devices.findIndex(
48 (device) => device.deviceId === selectedId
49 )
50 const selectedDevice = selectedIndex === -1 ? null : devices[selectedIndex]
51
52 return (
53 <div ref={rootRef} className="relative">
54 <Button
55 variant="neutral"
56 size="sm"
57 aria-expanded={open}
58 aria-controls={listId}
59 onClick={() => setOpen((value) => !value)}
60 >
61 <VideoCamera weight="bold" className="size-4" aria-hidden />
62 <span className="max-w-40 truncate">
63 {selectedDevice === null || selectedDevice.label === ""
64 ? "Kamera otomatis"
65 : selectedDevice.label}
66 </span>
67 <CaretDown weight="bold" className="size-4" aria-hidden />
68 </Button>
69
70 {open ? (
71 <ul
72 id={listId}
73 className="absolute right-0 z-20 mt-2 flex w-64 flex-col gap-1 rounded-card border-2 border-ink bg-paper p-2 shadow-hard"
74 >
75 {devices.map((device, index) => {
76 const label =
77 device.label === "" ? `Kamera ${index + 1}` : device.label
78 const selected = device.deviceId === selectedId
79
80 return (
81 <li key={device.deviceId}>
82 <button
83 type="button"
84 onClick={() => {
85 onSelect(device.deviceId)
86 setOpen(false)
87 }}
88 aria-pressed={selected}
89 className={cn(
90 "flex w-full items-center gap-2 rounded-thumb px-2 py-2 text-left text-[15px] font-bold",
91 "hover:bg-cream focus-visible:outline-3 focus-visible:outline-offset-1 focus-visible:outline-violet"
92 )}
93 >
94 <Check
95 weight="bold"
96 aria-hidden
97 className={cn(
98 "size-4 shrink-0 text-violet",
99 selected ? undefined : "invisible"
100 )}
101 />
102 <span className="truncate">{label}</span>
103 </button>
104 </li>
105 )
106 })}
107 </ul>
108 ) : null}
109 </div>
110 )
111}
112