{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "dropdown", "type": "registry:ui", "title": "Dropdown", "description": "A dropdown component that can be used to display a list of options.", "registryDependencies": [ "@tra-kit/popover" ], "files": [ { "path": "registry/tra-kit/components/dropdown.tsx", "content": "import React, { useEffect, useRef, useState } from 'react';\r\nimport { CaretDownIcon, CheckIcon } from '@phosphor-icons/react';\r\nimport { cn } from '@/lib/utils';\r\nimport { Popover, PopoverTrigger, PopoverContent } from './popover';\r\n\r\ninterface IDropdownItem {\r\n label: string | React.ReactNode;\r\n value: string | number | boolean;\r\n disabled?: boolean;\r\n}\r\n\r\ninterface IDropdownProps {\r\n label?: string;\r\n options: IDropdownItem[];\r\n value?: string | number | boolean;\r\n defaultValue?: string | number | boolean;\r\n onChange?: (value: string | number | boolean) => void;\r\n disabled?: boolean;\r\n className?: string;\r\n triggerClassName?: string;\r\n contentClassName?: string;\r\n itemClassName?: string;\r\n placeholder?: string;\r\n dropdownAlign?: 'left' | 'right';\r\n id?: string;\r\n iconClassName?: string;\r\n forceTriggerWidth?: boolean;\r\n}\r\n\r\nconst Dropdown: React.FC = ({\r\n label,\r\n options,\r\n value,\r\n defaultValue,\r\n onChange,\r\n disabled = false,\r\n className = '',\r\n triggerClassName = '',\r\n contentClassName = '',\r\n itemClassName = '',\r\n placeholder = 'Select...',\r\n dropdownAlign = 'left',\r\n id,\r\n iconClassName,\r\n forceTriggerWidth = false,\r\n}) => {\r\n const [open, setOpen] = useState(false);\r\n const [selected, setSelected] = useState(null);\r\n const [highlightedIndex, setHighlightedIndex] = useState(-1);\r\n const listRef = useRef(null);\r\n\r\n useEffect(() => {\r\n if (defaultValue !== undefined) {\r\n const def = options.find((o) => o.value === defaultValue) || null;\r\n setSelected(def);\r\n }\r\n }, [defaultValue, options]);\r\n\r\n useEffect(() => {\r\n if (value !== undefined) {\r\n const val = options.find((o) => o.value === value) || null;\r\n setSelected(val);\r\n }\r\n }, [value, options]);\r\n\r\n useEffect(() => {\r\n if (!open) {\r\n setHighlightedIndex(-1);\r\n }\r\n }, [open]);\r\n\r\n const onItemSelect = (item: IDropdownItem) => {\r\n if (item.disabled) return;\r\n setSelected(item);\r\n onChange?.(item.value);\r\n setOpen(false);\r\n };\r\n\r\n const handleKeyDown = (e: React.KeyboardEvent) => {\r\n if (!open) return;\r\n switch (e.key) {\r\n case 'ArrowDown':\r\n e.preventDefault();\r\n setHighlightedIndex((prev) => {\r\n const next = Math.min(prev + 1, options.length - 1);\r\n scrollIntoView(next);\r\n return next;\r\n });\r\n break;\r\n case 'ArrowUp':\r\n e.preventDefault();\r\n setHighlightedIndex((prev) => {\r\n const next = Math.max(prev - 1, 0);\r\n scrollIntoView(next);\r\n return next;\r\n });\r\n break;\r\n case 'Enter':\r\n e.preventDefault();\r\n if (highlightedIndex >= 0 && highlightedIndex < options.length) {\r\n onItemSelect(options[highlightedIndex]);\r\n }\r\n break;\r\n case 'Escape':\r\n setOpen(false);\r\n break;\r\n default:\r\n break;\r\n }\r\n };\r\n\r\n const scrollIntoView = (index: number) => {\r\n const node = listRef.current?.children[index] as HTMLElement | undefined;\r\n if (node) node.scrollIntoView({ block: 'nearest' });\r\n };\r\n\r\n return (\r\n \r\n \r\n {label &&
{label}
}\r\n \r\n
\r\n {selected ? selected.label : {placeholder}}\r\n
\r\n \r\n \r\n\r\n \r\n \r\n {options.map((opt, idx) => (\r\n onItemSelect(opt)}\r\n onMouseEnter={() => setHighlightedIndex(idx)}\r\n className={cn(\r\n 'flex items-center justify-between px-4 py-2 text-sm cursor-pointer',\r\n highlightedIndex === idx && 'bg-neutral-light',\r\n opt.disabled && 'opacity-50 cursor-not-allowed',\r\n itemClassName,\r\n )}\r\n >\r\n
{opt.label}
\r\n {selected?.value === opt.value && }\r\n \r\n ))}\r\n \r\n \r\n \r\n \r\n );\r\n};\r\n\r\nexport default Dropdown;\r\n", "type": "registry:ui" } ] }