{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "icon-picker",
  "type": "registry:ui",
  "title": "Icon Picker",
  "description": "Searchable icon grid and color swatches using an app-supplied icon catalog.",
  "dependencies": [
    "@base-ui/react@^1.8.0"
  ],
  "registryDependencies": [
    "https://control-ui.dev/r/core.json",
    "https://control-ui.dev/r/input.json"
  ],
  "files": [
    {
      "path": "src/registry/knob-contracts/icon-picker-knobs.ts",
      "target": "@components/control-ui/knob-contracts/icon-picker-knobs.ts",
      "type": "registry:component",
      "content": "// Generated from src/registry/sources/control-ui/recipes/icon-picker.css by scripts/gen-knob-contracts.ts — run `bun run sync:knobs`.\nexport const iconPickerKnobs = [\n  \"--cui-icon-picker-cell-size\",\n  \"--cui-icon-picker-cell-radius\",\n  \"--cui-icon-picker-foreground\",\n  \"--cui-icon-picker-active-background\",\n] as const;\nexport type IconPickerKnobStyle = Partial<Record<(typeof iconPickerKnobs)[number], string>>;\n"
    },
    {
      "path": "src/registry/sources/control-ui/recipes/icon-picker.css",
      "target": "@components/control-ui/styles/recipes/icon-picker.css",
      "type": "registry:file",
      "content": "@layer components {\n  :where([data-control-family=\"icon-picker\"][data-slot=\"root\"]) {\n    --cui-icon-picker-cell-size: calc(var(--spacing) * 10);\n    --cui-icon-picker-cell-radius: var(--radius-control);\n    --cui-icon-picker-foreground: var(--foreground);\n    --cui-icon-picker-active-background: var(--accent);\n    inline-size: calc(var(--_icon-picker-columns) * var(--cui-icon-picker-cell-size) + var(--spacing) * 4);\n    max-inline-size: 100%;\n    color: var(--cui-icon-picker-foreground);\n  }\n  :where([data-control-family=\"icon-picker\"][data-slot=\"content\"]) {\n    padding: calc(var(--spacing) * 2);\n    max-block-size: 16rem;\n    overflow-y: auto;\n    overscroll-behavior: contain;\n    scrollbar-width: thin;\n  }\n  :where([data-control-family=\"icon-picker\"][data-slot=\"grid\"]) {\n    display: grid;\n    grid-template-columns: repeat(var(--_icon-picker-columns), minmax(0, 1fr));\n  }\n  :where([data-control-family=\"icon-picker\"][data-slot=\"icon\"]) {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    block-size: var(--cui-icon-picker-cell-size);\n    min-inline-size: 0;\n    border-radius: var(--cui-icon-picker-cell-radius);\n    background: transparent;\n    color: inherit;\n    cursor: pointer;\n    transition: background-color var(--duration-fast) var(--ease-standard);\n  }\n  :where([data-control-family=\"icon-picker\"][data-slot=\"icon\"]:is(:hover, [aria-pressed=\"true\"])) {\n    background: var(--cui-icon-picker-active-background);\n  }\n  :where([data-control-family=\"icon-picker\"][data-slot=\"icon\"] svg) {\n    inline-size: calc(var(--spacing) * 5);\n    block-size: calc(var(--spacing) * 5);\n  }\n  :where([data-control-family=\"icon-picker\"][data-slot=\"colors\"]) {\n    display: flex;\n    gap: calc(var(--spacing) * 1);\n    padding: calc(var(--spacing) * 1) calc(var(--spacing) * 2) 0;\n    overflow-x: auto;\n  }\n  :where([data-control-family=\"icon-picker\"][data-slot=\"color\"]) {\n    display: grid;\n    place-items: center;\n    flex: 1 0 calc(var(--spacing) * 9);\n    block-size: calc(var(--spacing) * 10);\n    border-radius: var(--cui-icon-picker-cell-radius, var(--radius-control));\n    cursor: pointer;\n  }\n  :where([data-control-family=\"icon-picker\"][data-slot=\"color\"] > span) {\n    inline-size: calc(var(--spacing) * 6);\n    block-size: calc(var(--spacing) * 6);\n    border-radius: 50%;\n    background: currentColor;\n  }\n  :where([data-control-family=\"icon-picker\"][data-slot=\"color\"][data-checked] > span) {\n    outline: 2px solid var(--foreground);\n    outline-offset: 3px;\n  }\n  :where([data-control-family=\"icon-picker\"][data-slot=\"empty\"]) {\n    display: grid;\n    place-items: center;\n    min-block-size: calc(var(--spacing) * 20);\n    color: var(--muted-foreground);\n    font-size: var(--text-label);\n  }\n  @media (pointer: coarse) {\n    :where([data-control-family=\"icon-picker\"][data-slot=\"root\"]) {\n      --cui-icon-picker-cell-size: 44px;\n    }\n    :where([data-control-family=\"icon-picker\"][data-slot=\"color\"]) {\n      min-inline-size: 44px;\n      block-size: 44px;\n    }\n  }\n}\n@property --cui-icon-picker-cell-size {\n  syntax: \"<length>\";\n  inherits: true;\n  initial-value: 0px;\n}\n@property --cui-icon-picker-cell-radius {\n  syntax: \"<length-percentage>\";\n  inherits: true;\n  initial-value: 0px;\n}\n@property --cui-icon-picker-foreground {\n  syntax: \"<color>\";\n  inherits: true;\n  initial-value: transparent;\n}\n@property --cui-icon-picker-active-background {\n  syntax: \"<color>\";\n  inherits: true;\n  initial-value: transparent;\n}\n"
    },
    {
      "path": "src/registry/sources/control-ui/ui/icon-picker.tsx",
      "target": "@components/control-ui/ui/icon-picker.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport { Radio as RadioPrimitive } from \"@base-ui/react/radio\";\nimport { RadioGroup as RadioGroupPrimitive } from \"@base-ui/react/radio-group\";\nimport { Toolbar as ToolbarPrimitive } from \"@base-ui/react/toolbar\";\nimport {\n  type ComponentProps,\n  type CSSProperties,\n  createContext,\n  type KeyboardEvent,\n  type ReactNode,\n  useContext,\n  useRef,\n  useState,\n} from \"react\";\nimport type { ControlledChoice } from \"@/components/control-ui/control-props\";\nimport type { IconPickerKnobStyle } from \"@/components/control-ui/knob-contracts/icon-picker-knobs\";\nimport { cn } from \"@/components/control-ui/lib/cn\";\nimport { Input } from \"@/components/control-ui/ui/input\";\n\nexport type IconPickerItem = { value: string; label: string; icon: ReactNode; keywords?: readonly string[] };\nexport type IconPickerColor = { value: string; label: string; color: string };\ntype IconPickerStyle = { style?: CSSProperties & IconPickerKnobStyle };\ntype IconPickerContextValue = {\n  items: readonly IconPickerItem[];\n  columns: number;\n  selectedValue: string | undefined;\n  selectIcon: (value: string) => void;\n  search: string;\n  setSearch: (search: string) => void;\n};\n\nconst IconPickerContext = createContext<IconPickerContextValue | null>(null);\n\nfunction useIconPicker() {\n  const picker = useContext(IconPickerContext);\n  if (!picker) throw new Error(\"IconPicker parts must be rendered inside <IconPicker>.\");\n  return picker;\n}\n\nexport type IconPickerProps = Omit<ComponentProps<\"div\">, \"style\" | \"defaultValue\" | \"onChange\"> &\n  IconPickerStyle &\n  ControlledChoice & {\n    items: readonly IconPickerItem[];\n    columns?: number;\n  };\n\nexport function IconPicker({\n  items,\n  columns = 8,\n  value,\n  defaultValue,\n  onValueChange,\n  className,\n  style,\n  children,\n  ...props\n}: IconPickerProps) {\n  const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);\n  const [search, setSearch] = useState(\"\");\n  const selectIcon = (selectedValue: string) => {\n    if (value === undefined) setUncontrolledValue(selectedValue);\n    onValueChange?.(selectedValue);\n  };\n  const pickerStyle = { \"--_icon-picker-columns\": columns, ...style } satisfies CSSProperties & { \"--_icon-picker-columns\": number };\n\n  return (\n    <IconPickerContext.Provider value={{ items, columns, selectedValue: value ?? uncontrolledValue, selectIcon, search, setSearch }}>\n      <div\n        data-control-ui=\"icon-picker\"\n        data-control-family=\"icon-picker\"\n        data-slot=\"root\"\n        className={className}\n        style={pickerStyle}\n        {...props}\n      >\n        {children}\n      </div>\n    </IconPickerContext.Provider>\n  );\n}\n\nexport type IconPickerSearchProps = Omit<ComponentProps<typeof Input>, \"value\" | \"defaultValue\" | \"onValueChange\">;\n\nexport function IconPickerSearch({\n  placeholder = \"Search icons…\",\n  \"aria-label\": ariaLabel = \"Search icons\",\n  className,\n  onChange,\n  ...props\n}: IconPickerSearchProps) {\n  const picker = useIconPicker();\n  return (\n    <Input\n      type=\"search\"\n      size=\"sm\"\n      placeholder={placeholder}\n      aria-label={ariaLabel}\n      autoComplete=\"off\"\n      spellCheck={false}\n      className={cn(\"mx-2 mt-2 w-[calc(100%-var(--spacing)*4)]\", className)}\n      {...props}\n      value={picker.search}\n      onChange={(event) => {\n        onChange?.(event);\n        if (!event.defaultPrevented) picker.setSearch(event.target.value);\n      }}\n    />\n  );\n}\n\nexport type IconPickerContentProps = Omit<ComponentProps<\"div\">, \"style\" | \"children\"> & IconPickerStyle & { empty?: ReactNode };\n\nexport function IconPickerContent({\n  className,\n  empty = \"No icons found.\",\n  \"aria-label\": ariaLabel = \"Icons\",\n  ...props\n}: IconPickerContentProps) {\n  const picker = useIconPicker();\n  const buttonsRef = useRef(new Map<string, HTMLButtonElement>());\n  const normalizedSearch = picker.search.trim().toLocaleLowerCase();\n  const filteredItems = picker.items.filter((item) =>\n    [item.label, ...(item.keywords ?? [])].some((text) => text.toLocaleLowerCase().includes(normalizedSearch)),\n  );\n\n  const moveGridFocus = (event: KeyboardEvent<HTMLDivElement>) => {\n    if (event.key !== \"ArrowUp\" && event.key !== \"ArrowDown\") return;\n    const currentIndex = filteredItems.findIndex((item) => buttonsRef.current.get(item.value) === event.target);\n    if (currentIndex < 0) return;\n    event.preventDefault();\n    const direction = event.key === \"ArrowDown\" ? 1 : -1;\n    const nextIndex = Math.max(0, Math.min(filteredItems.length - 1, currentIndex + direction * picker.columns));\n    buttonsRef.current.get(filteredItems[nextIndex].value)?.focus();\n  };\n\n  return (\n    <div data-control-ui=\"icon-picker\" data-control-family=\"icon-picker\" data-slot=\"content\" className={className} {...props}>\n      {filteredItems.length === 0 ? (\n        <div data-control-ui=\"icon-picker\" data-control-family=\"icon-picker\" data-slot=\"empty\" role=\"status\">\n          {empty}\n        </div>\n      ) : (\n        <ToolbarPrimitive.Root\n          data-control-ui=\"icon-picker\"\n          data-control-family=\"icon-picker\"\n          data-slot=\"grid\"\n          aria-label={ariaLabel}\n          loopFocus={false}\n          onKeyDownCapture={moveGridFocus}\n        >\n          {filteredItems.map((item) => (\n            <ToolbarPrimitive.Button\n              key={item.value}\n              type=\"button\"\n              aria-label={item.label}\n              aria-pressed={picker.selectedValue === item.value}\n              data-control-ui=\"icon-picker\"\n              data-control-family=\"icon-picker\"\n              data-slot=\"icon\"\n              ref={(button) => {\n                if (!button) return;\n                buttonsRef.current.set(item.value, button);\n                return () => {\n                  buttonsRef.current.delete(item.value);\n                };\n              }}\n              onClick={() => picker.selectIcon(item.value)}\n            >\n              <span aria-hidden=\"true\">{item.icon}</span>\n            </ToolbarPrimitive.Button>\n          ))}\n        </ToolbarPrimitive.Root>\n      )}\n    </div>\n  );\n}\n\nexport type IconPickerColorsProps = Omit<ComponentProps<\"div\">, \"style\" | \"defaultValue\" | \"onChange\" | \"children\"> &\n  IconPickerStyle &\n  ControlledChoice & {\n    colors: readonly IconPickerColor[];\n  };\n\nexport function IconPickerColors({ colors, \"aria-label\": ariaLabel = \"Icon color\", ...props }: IconPickerColorsProps) {\n  return (\n    <RadioGroupPrimitive\n      data-control-ui=\"icon-picker\"\n      data-control-family=\"icon-picker\"\n      data-slot=\"colors\"\n      aria-label={ariaLabel}\n      {...props}\n    >\n      {colors.map((color) => (\n        <RadioPrimitive.Root\n          key={color.value}\n          value={color.value}\n          aria-label={color.label}\n          data-control-ui=\"icon-picker\"\n          data-control-family=\"icon-picker\"\n          data-slot=\"color\"\n          style={{ color: color.color }}\n        >\n          <span aria-hidden=\"true\" />\n        </RadioPrimitive.Root>\n      ))}\n    </RadioGroupPrimitive>\n  );\n}\n"
    }
  ],
  "css": {
    "@import \"../components/control-ui/styles/recipes/icon-picker.css\"": {}
  }
}
