{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "select-styled",
  "title": "Select essentials",
  "description": "Styled select: the essentials set. 5 decorative select variations (SimpleSelect, GlassSelect, IconSelect, PillSelect, GroupedSelect) on the ai2 token system, powered by framer-motion, reduced-motion aware and sized sm to xl. Part of the free styled layer.",
  "dependencies": [
    "motion@^12.42.2",
    "lucide-react@^1.23.0"
  ],
  "registryDependencies": [
    "@ai2/tokens"
  ],
  "files": [
    {
      "path": "registry/ai2/styled/select-styled.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Check, ChevronDown, CreditCard, Settings, Star, User } from \"lucide-react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/* Select family: 5 decorative self-contained single-selects. NO radix, NO native\n   <select>, NO portal. Each export is a complete select: a relative inline-flex\n   wrapper + a real trigger button (the selected value + a chevron) + an options\n   panel absolutely positioned BELOW the trigger (top-full mt-1). The trigger opens\n   on CLICK; picking an option updates the value and closes the panel. It closes on\n   an outside click (window pointerdown, with inner clicks ignored via the wrapper\n   ref) and on Escape. Controlled (value/onValueChange) + uncontrolled\n   (defaultValue). AnimatePresence fade+scale; under reduced motion only a fade\n   (instant). Color comes ONLY from tokens, via alpha color-mix. */\n\nexport type StyledSize = \"sm\" | \"md\" | \"lg\" | \"xl\"\n\n/* Trigger yuksekligi base kontrol olcegiyle hizali (sm h-8, md h-9, lg h-10, xl h-12). */\nconst triggerHeight: Record<StyledSize, string> = {\n  sm: \"h-8 text-sm\",\n  md: \"h-9 text-sm\",\n  lg: \"h-10 text-base\",\n  xl: \"h-12 text-base\",\n}\n\n/* Panel genisligi trigger olcegine gore. */\nconst panelWidth: Record<StyledSize, string> = {\n  sm: \"w-48\",\n  md: \"w-56\",\n  lg: \"w-64\",\n  xl: \"w-72\",\n}\n\nexport interface StyledSelectOption {\n  value: string\n  label: React.ReactNode\n  icon?: React.ReactNode\n}\n\nexport interface StyledSelectGroup {\n  heading: React.ReactNode\n  options: StyledSelectOption[]\n}\n\ninterface SelectProps {\n  className?: string\n  size?: StyledSize\n  placeholder?: string\n  options?: StyledSelectOption[]\n  value?: string\n  defaultValue?: string\n  onValueChange?: (v: string) => void\n}\n\nconst defaultOptions: StyledSelectOption[] = [\n  { value: \"light\", label: \"Light\" },\n  { value: \"dark\", label: \"Dark\" },\n  { value: \"system\", label: \"System\" },\n  { value: \"auto\", label: \"Automatic\" },\n]\n\nconst triggerBase =\n  \"inline-flex w-full shrink-0 select-none items-center justify-between gap-2 whitespace-nowrap rounded-lg px-3 font-medium outline-none transition-colors focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0 [&_i]:text-base [&_i]:leading-none\"\n\nconst panelBase =\n  \"absolute left-0 top-full z-50 mt-1 max-h-72 origin-top overflow-auto rounded-xl border border-border bg-popover p-1.5 text-sm text-popover-foreground shadow-lg outline-none\"\n\nconst optionBase =\n  \"flex w-full cursor-default select-none items-center gap-2.5 rounded-md px-3 py-2 text-left text-sm text-popover-foreground outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:bg-accent focus-visible:text-accent-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-selected:bg-accent aria-selected:text-accent-foreground [&_svg]:size-4 [&_svg]:shrink-0 [&_i]:text-base [&_i]:leading-none\"\n\nconst panelMotion = {\n  initial: { opacity: 0, scale: 0.96, y: -6 },\n  animate: { opacity: 1, scale: 1, y: 0 },\n  exit: { opacity: 0, scale: 0.96, y: -6 },\n  transition: { type: \"spring\" as const, stiffness: 340, damping: 26 },\n}\n\n/* Internal open/closed state: it closes on an outside click + Escape. */\nfunction usePanelState() {\n  const [open, setOpen] = React.useState(false)\n  const wrapperRef = React.useRef<HTMLDivElement>(null)\n\n  React.useEffect(() => {\n    if (!open) return\n    const onKey = (e: KeyboardEvent) => {\n      if (e.key === \"Escape\") setOpen(false)\n    }\n    const onPointer = (e: PointerEvent) => {\n      const node = wrapperRef.current\n      if (node && e.target instanceof Node && !node.contains(e.target)) {\n        setOpen(false)\n      }\n    }\n    window.addEventListener(\"keydown\", onKey)\n    window.addEventListener(\"pointerdown\", onPointer)\n    return () => {\n      window.removeEventListener(\"keydown\", onKey)\n      window.removeEventListener(\"pointerdown\", onPointer)\n    }\n  }, [open])\n\n  return { open, setOpen, wrapperRef }\n}\n\n/* Controlled and uncontrolled value management. */\nfunction useSelectValue(\n  value: string | undefined,\n  defaultValue: string | undefined,\n  onValueChange: ((v: string) => void) | undefined,\n) {\n  const [internal, setInternal] = React.useState<string | undefined>(defaultValue)\n  const isControlled = value !== undefined\n  const current = isControlled ? value : internal\n\n  const select = React.useCallback(\n    (next: string) => {\n      if (!isControlled) setInternal(next)\n      onValueChange?.(next)\n    },\n    [isControlled, onValueChange],\n  )\n\n  return { current, select }\n}\n\nfunction flattenOptions(\n  options: StyledSelectOption[] | undefined,\n  groups?: StyledSelectGroup[],\n): StyledSelectOption[] {\n  if (groups) return groups.flatMap((g) => g.options)\n  return options ?? defaultOptions\n}\n\n/* Shared shell: a relative wrapper plus a trigger button plus an AnimatePresence options panel. */\nfunction SelectShell({\n  size = \"md\",\n  className,\n  open,\n  setOpen,\n  wrapperRef,\n  triggerClassName,\n  panelClassName,\n  triggerContent,\n  children,\n}: {\n  size?: StyledSize\n  className?: string\n  open: boolean\n  setOpen: (v: boolean) => void\n  wrapperRef: React.RefObject<HTMLDivElement | null>\n  triggerClassName: string\n  panelClassName?: string\n  triggerContent: React.ReactNode\n  children: React.ReactNode\n}) {\n  const reduce = useReducedMotion()\n  const fade = { initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 } }\n  const active = reduce ? fade : panelMotion\n\n  return (\n    <div\n      ref={wrapperRef}\n      data-slot=\"styled-select\"\n      className={cn(\"relative inline-flex w-56 max-w-full\", panelWidth[size], className)}\n    >\n      <button\n        type=\"button\"\n        data-slot=\"styled-select-trigger\"\n        role=\"combobox\"\n        aria-expanded={open}\n        aria-haspopup=\"listbox\"\n        className={cn(triggerBase, triggerHeight[size], triggerClassName)}\n        onClick={() => setOpen(!open)}\n      >\n        {triggerContent}\n        <ChevronDown\n          className={cn(\n            \"shrink-0 opacity-70 transition-transform duration-200\",\n            open && \"rotate-180\",\n          )}\n        />\n      </button>\n\n      <AnimatePresence>\n        {open ? (\n          <motion.div\n            role=\"listbox\"\n            data-slot=\"styled-select-content\"\n            className={cn(panelBase, panelWidth[size], panelClassName)}\n            initial={active.initial}\n            animate={active.animate}\n            exit={active.exit}\n            transition={reduce ? { duration: 0.16 } : panelMotion.transition}\n          >\n            {children}\n          </motion.div>\n        ) : null}\n      </AnimatePresence>\n    </div>\n  )\n}\n\n/* Finds the label of the selected option (to show it in the trigger). */\nfunction labelFor(options: StyledSelectOption[], current: string | undefined) {\n  if (current === undefined) return undefined\n  return options.find((o) => o.value === current)?.label\n}\n\n/* Simple: sade cerceveli trigger + liste. */\nexport function SimpleSelect({\n  className,\n  size = \"md\",\n  placeholder = \"Select...\",\n  options,\n  value,\n  defaultValue,\n  onValueChange,\n}: SelectProps) {\n  const list = options ?? defaultOptions\n  const { open, setOpen, wrapperRef } = usePanelState()\n  const { current, select } = useSelectValue(value, defaultValue, onValueChange)\n  const selectedLabel = labelFor(list, current)\n\n  return (\n    <SelectShell\n      size={size}\n      className={className}\n      open={open}\n      setOpen={setOpen}\n      wrapperRef={wrapperRef}\n      triggerClassName=\"border border-field-border bg-background text-foreground hover:bg-accent hover:text-accent-foreground\"\n      triggerContent={\n        <span className={cn(\"truncate\", selectedLabel === undefined && \"text-muted-foreground\")}>\n          {selectedLabel ?? placeholder}\n        </span>\n      }\n    >\n      <div className=\"flex flex-col\">\n        {list.map((option) => {\n          const isSelected = option.value === current\n          return (\n            <button\n              key={option.value}\n              type=\"button\"\n              role=\"option\"\n              aria-selected={isSelected}\n              tabIndex={0}\n              className={cn(optionBase)}\n              onClick={() => {\n                select(option.value)\n                setOpen(false)\n              }}\n            >\n              <span className=\"flex size-4 shrink-0 items-center justify-center text-primary\">\n                {isSelected ? <Check /> : null}\n              </span>\n              <span className=\"truncate\">{option.label}</span>\n            </button>\n          )\n        })}\n      </div>\n    </SelectShell>\n  )\n}\n\n/* Glass: buzlu cam trigger + panel (frosted). */\nexport function GlassSelect({\n  className,\n  size = \"md\",\n  placeholder = \"Select...\",\n  options,\n  value,\n  defaultValue,\n  onValueChange,\n}: SelectProps) {\n  const list = options ?? defaultOptions\n  const { open, setOpen, wrapperRef } = usePanelState()\n  const { current, select } = useSelectValue(value, defaultValue, onValueChange)\n  const selectedLabel = labelFor(list, current)\n\n  return (\n    <SelectShell\n      size={size}\n      className={className}\n      open={open}\n      setOpen={setOpen}\n      wrapperRef={wrapperRef}\n      triggerClassName=\"border border-[color-mix(in_oklab,var(--color-border)_60%,transparent)] bg-[color-mix(in_oklab,var(--color-background)_55%,transparent)] text-foreground backdrop-blur-md hover:bg-[color-mix(in_oklab,var(--color-background)_70%,transparent)]\"\n      panelClassName=\"border-[color-mix(in_oklab,var(--color-border)_60%,transparent)] bg-[color-mix(in_oklab,var(--color-popover)_70%,transparent)] backdrop-blur-xl\"\n      triggerContent={\n        <span className={cn(\"truncate\", selectedLabel === undefined && \"text-muted-foreground\")}>\n          {selectedLabel ?? placeholder}\n        </span>\n      }\n    >\n      <div className=\"flex flex-col\">\n        {list.map((option) => {\n          const isSelected = option.value === current\n          return (\n            <button\n              key={option.value}\n              type=\"button\"\n              role=\"option\"\n              aria-selected={isSelected}\n              tabIndex={0}\n              className={cn(optionBase)}\n              onClick={() => {\n                select(option.value)\n                setOpen(false)\n              }}\n            >\n              <span className=\"flex size-4 shrink-0 items-center justify-center text-primary\">\n                {isSelected ? <Check /> : null}\n              </span>\n              <span className=\"truncate\">{option.label}</span>\n            </button>\n          )\n        })}\n      </div>\n    </SelectShell>\n  )\n}\n\nconst iconOptions: StyledSelectOption[] = [\n  { value: \"account\", label: \"Account\", icon: <User /> },\n  { value: \"billing\", label: \"Billing\", icon: <CreditCard /> },\n  { value: \"settings\", label: \"Settings\", icon: <Settings /> },\n  { value: \"starred\", label: \"Starred\", icon: <Star /> },\n]\n\n/* Icon: every option carries a leading token icon; the selected value shows its icon in the trigger too. */\nexport function IconSelect({\n  className,\n  size = \"md\",\n  placeholder = \"Select...\",\n  options,\n  value,\n  defaultValue,\n  onValueChange,\n}: SelectProps) {\n  const list = options ?? iconOptions\n  const { open, setOpen, wrapperRef } = usePanelState()\n  const { current, select } = useSelectValue(value, defaultValue, onValueChange)\n  const selected = list.find((o) => o.value === current)\n\n  return (\n    <SelectShell\n      size={size}\n      className={className}\n      open={open}\n      setOpen={setOpen}\n      wrapperRef={wrapperRef}\n      triggerClassName=\"border border-field-border bg-background text-foreground hover:bg-accent hover:text-accent-foreground\"\n      triggerContent={\n        <span className=\"flex min-w-0 items-center gap-2\">\n          {selected?.icon ? (\n            <span className=\"flex shrink-0 items-center text-muted-foreground\">{selected.icon}</span>\n          ) : null}\n          <span className={cn(\"truncate\", selected === undefined && \"text-muted-foreground\")}>\n            {selected?.label ?? placeholder}\n          </span>\n        </span>\n      }\n    >\n      <div className=\"flex flex-col\">\n        {list.map((option) => {\n          const isSelected = option.value === current\n          return (\n            <button\n              key={option.value}\n              type=\"button\"\n              role=\"option\"\n              aria-selected={isSelected}\n              tabIndex={0}\n              className={cn(optionBase)}\n              onClick={() => {\n                select(option.value)\n                setOpen(false)\n              }}\n            >\n              {option.icon ? (\n                <span className=\"flex shrink-0 items-center text-muted-foreground\">\n                  {option.icon}\n                </span>\n              ) : null}\n              <span className=\"flex-1 truncate\">{option.label}</span>\n              {isSelected ? (\n                <Check className=\"shrink-0 text-primary\" />\n              ) : null}\n            </button>\n          )\n        })}\n      </div>\n    </SelectShell>\n  )\n}\n\n/* Pill: tamamen yuvarlak (rounded-full) pill trigger. */\nexport function PillSelect({\n  className,\n  size = \"md\",\n  placeholder = \"Select...\",\n  options,\n  value,\n  defaultValue,\n  onValueChange,\n}: SelectProps) {\n  const list = options ?? defaultOptions\n  const { open, setOpen, wrapperRef } = usePanelState()\n  const { current, select } = useSelectValue(value, defaultValue, onValueChange)\n  const selectedLabel = labelFor(list, current)\n\n  return (\n    <SelectShell\n      size={size}\n      className={className}\n      open={open}\n      setOpen={setOpen}\n      wrapperRef={wrapperRef}\n      triggerClassName=\"rounded-full border border-field-border bg-secondary px-4 text-secondary-foreground hover:bg-[color-mix(in_oklab,var(--color-foreground)_6%,transparent)]\"\n      panelClassName=\"rounded-2xl\"\n      triggerContent={\n        <span className={cn(\"truncate\", selectedLabel === undefined && \"text-muted-foreground\")}>\n          {selectedLabel ?? placeholder}\n        </span>\n      }\n    >\n      <div className=\"flex flex-col\">\n        {list.map((option) => {\n          const isSelected = option.value === current\n          return (\n            <button\n              key={option.value}\n              type=\"button\"\n              role=\"option\"\n              aria-selected={isSelected}\n              tabIndex={0}\n              className={cn(optionBase, \"rounded-full\")}\n              onClick={() => {\n                select(option.value)\n                setOpen(false)\n              }}\n            >\n              <span className=\"flex size-4 shrink-0 items-center justify-center text-primary\">\n                {isSelected ? <Check /> : null}\n              </span>\n              <span className=\"truncate\">{option.label}</span>\n            </button>\n          )\n        })}\n      </div>\n    </SelectShell>\n  )\n}\n\nconst groupedData: StyledSelectGroup[] = [\n  {\n    heading: \"Fruits\",\n    options: [\n      { value: \"apple\", label: \"Apple\" },\n      { value: \"banana\", label: \"Banana\" },\n      { value: \"orange\", label: \"Orange\" },\n    ],\n  },\n  {\n    heading: \"Vegetables\",\n    options: [\n      { value: \"carrot\", label: \"Carrot\" },\n      { value: \"spinach\", label: \"Spinach\" },\n    ],\n  },\n]\n\ninterface GroupedProps extends Omit<SelectProps, \"options\"> {\n  groups?: StyledSelectGroup[]\n}\n\n/* Grouped: option'lar token label header'lar altinda gruplanir. */\nexport function GroupedSelect({\n  className,\n  size = \"md\",\n  placeholder = \"Select...\",\n  groups,\n  value,\n  defaultValue,\n  onValueChange,\n}: GroupedProps) {\n  const data = groups ?? groupedData\n  const { open, setOpen, wrapperRef } = usePanelState()\n  const { current, select } = useSelectValue(value, defaultValue, onValueChange)\n  const flat = flattenOptions(undefined, data)\n  const selectedLabel = labelFor(flat, current)\n\n  return (\n    <SelectShell\n      size={size}\n      className={className}\n      open={open}\n      setOpen={setOpen}\n      wrapperRef={wrapperRef}\n      triggerClassName=\"border border-field-border bg-background text-foreground hover:bg-accent hover:text-accent-foreground\"\n      triggerContent={\n        <span className={cn(\"truncate\", selectedLabel === undefined && \"text-muted-foreground\")}>\n          {selectedLabel ?? placeholder}\n        </span>\n      }\n    >\n      <div className=\"flex flex-col\">\n        {data.map((group, gi) => (\n          <div key={`${gi}`} className=\"flex flex-col\">\n            {gi > 0 ? (\n              <div\n                role=\"separator\"\n                className=\"my-1.5 h-px bg-[color-mix(in_oklab,var(--color-border)_100%,transparent)]\"\n              />\n            ) : null}\n            <div className=\"px-3 py-1.5 text-xs font-medium text-muted-foreground\">\n              {group.heading}\n            </div>\n            {group.options.map((option) => {\n              const isSelected = option.value === current\n              return (\n                <button\n                  key={option.value}\n                  type=\"button\"\n                  role=\"option\"\n                  aria-selected={isSelected}\n                  tabIndex={0}\n                  className={cn(optionBase)}\n                  onClick={() => {\n                    select(option.value)\n                    setOpen(false)\n                  }}\n                >\n                  <span className=\"flex size-4 shrink-0 items-center justify-center text-primary\">\n                    {isSelected ? <Check /> : null}\n                  </span>\n                  <span className=\"truncate\">{option.label}</span>\n                </button>\n              )\n            })}\n          </div>\n        ))}\n      </div>\n    </SelectShell>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/ui/select-styled.tsx"
    }
  ],
  "categories": [
    "styled",
    "select"
  ],
  "type": "registry:component"
}