{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "tag-cloud-select", "type": "registry:ui", "title": "Tag Cloud Select", "description": "Interactive tag selection component with cloud-style layout and multi-select functionality.", "dependencies": [ "lucide-react" ], "registryDependencies": [ "popover", "select", "button", "scroll-area" ], "files": [ { "path": "registry/ruixenui/tag-cloud-select.tsx", "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { X } from \"lucide-react\";\nimport {\n Popover,\n PopoverTrigger,\n PopoverContent,\n} from \"@/components/ui/popover\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { ScrollArea } from \"@/components/ui/scroll-area\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface TagCloudOption {\n value: string;\n label: string;\n popularity: number; // 1–100 to indicate frequency\n color?: string; // Optional custom color\n}\n\ninterface TagCloudSelectProps {\n options: TagCloudOption[];\n placeholder?: string;\n onChange?: (selected: string[]) => void;\n defaultSelected?: string[];\n minFontSize?: number; // e.g., 12\n maxFontSize?: number; // e.g., 28\n showSearch?: boolean;\n}\n\nexport const TagCloudSelect: React.FC = ({\n options,\n placeholder = \"Select tags...\",\n onChange,\n defaultSelected = [],\n minFontSize = 12,\n maxFontSize = 28,\n showSearch = true,\n}) => {\n const [open, setOpen] = React.useState(false);\n const [selected, setSelected] = React.useState(defaultSelected);\n const [searchTerm, setSearchTerm] = React.useState(\"\");\n\n const handleSelect = (value: string) => {\n setSelected((prev) => {\n const newSelected = prev.includes(value)\n ? prev.filter((v) => v !== value)\n : [...prev, value];\n onChange?.(newSelected);\n return newSelected;\n });\n };\n\n const handleRemove = (value: string) => {\n setSelected((prev) => {\n const newSelected = prev.filter((v) => v !== value);\n onChange?.(newSelected);\n return newSelected;\n });\n };\n\n const getFontSize = (popularity: number) => {\n const clamped = Math.max(1, Math.min(100, popularity));\n return `${minFontSize + ((maxFontSize - minFontSize) * clamped) / 100}px`;\n };\n\n const filteredOptions = options.filter((opt) =>\n opt.label.toLowerCase().includes(searchTerm.toLowerCase()),\n );\n\n return (\n \n \n \n \n \n {showSearch && (\n setSearchTerm(e.target.value)}\n className=\"mb-3\"\n />\n )}\n \n
\n {filteredOptions.map((opt) => (\n handleSelect(opt.value)}\n className={cn(\n \"transition-all rounded-full px-3 py-1 font-medium cursor-pointer border\",\n selected.includes(opt.value)\n ? \"bg-primary text-primary-foreground border-primary\"\n : \"bg-gray-100 hover:bg-gray-200 dark:bg-black dark:hover:bg-gray-950\",\n )}\n style={{\n fontSize: getFontSize(opt.popularity),\n color: !selected.includes(opt.value)\n ? opt.color || \"inherit\"\n : undefined,\n }}\n >\n {opt.label}\n \n ))}\n {filteredOptions.length === 0 && (\n

No tags found.

\n )}\n
\n
\n\n {selected.length > 0 && (\n
\n {selected.map((val) => {\n const tag = options.find((o) => o.value === val);\n return (\n \n {tag?.label}\n handleRemove(val)}\n />\n \n );\n })}\n
\n )}\n
\n
\n );\n};\n", "type": "registry:ui", "target": "components/ruixen/tag-cloud-select.tsx" } ] }