{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "select-rich",
  "title": "Rich select",
  "description": "Styled select: the Rich set. 5 decorative select variations (AvatarSelect, DescriptionSelect, BadgeSelect, MediaSelect, SplitSelect) 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-rich.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Check, ChevronDown } from \"lucide-react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/* Rich select family: 5 self-contained single-selects whose option rows are richer\n   than plain text (an initial-based avatar, a description line, a badge, an inline\n   SVG media square, meta on the right). There are NO remote image URLs: every\n   surface is painted with tokens and the glyphs are inline SVG. NO radix, NO portal.\n   Keyboard: ArrowUp/Down moves the highlight, Enter selects, Escape closes and\n   returns focus to the trigger. Color comes ONLY from tokens, via alpha\n   color-mix. */\n\nexport type StyledSize = \"sm\" | \"md\" | \"lg\" | \"xl\"\n\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\nconst panelWidth: Record<StyledSize, string> = {\n  sm: \"w-56\",\n  md: \"w-64\",\n  lg: \"w-72\",\n  xl: \"w-80\",\n}\n\nexport interface StyledRichOption {\n  value: string\n  label: string\n  /** The second description line (the description variant). */\n  description?: string\n  /** Sag tarafta kucuk rozet metni (badge varyanti). */\n  badge?: string\n  /** The initials in the avatar square; derived from the label when not given. */\n  initials?: string\n  /** Sag tarafta ikincil meta metni (split varyanti). */\n  meta?: string\n}\n\nexport interface StyledSelectProps {\n  className?: string\n  size?: StyledSize\n  placeholder?: string\n  options?: StyledRichOption[]\n  value?: string\n  defaultValue?: string\n  onValueChange?: (v: string) => void\n}\n\nconst defaultOptions: StyledRichOption[] = [\n  { value: \"ada\", label: \"Ada Lovelace\", description: \"Owner\", badge: \"Admin\", meta: \"Owner\", initials: \"AL\" },\n  { value: \"grace\", label: \"Grace Hopper\", description: \"Can edit and publish\", badge: \"Editor\", meta: \"Editor\", initials: \"GH\" },\n  { value: \"alan\", label: \"Alan Turing\", description: \"Can comment on drafts\", badge: \"Viewer\", meta: \"Viewer\", initials: \"AT\" },\n  { value: \"katherine\", label: \"Katherine Johnson\", description: \"Read-only access\", badge: \"Guest\", meta: \"Guest\", initials: \"KJ\" },\n]\n\nconst triggerBase =\n  \"inline-flex w-full shrink-0 select-none items-center justify-between gap-2 whitespace-nowrap rounded-lg border border-field-border bg-background px-3 font-medium text-foreground outline-none transition-colors hover:bg-accent hover:text-accent-foreground 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-80 min-w-56 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-3 rounded-md px-2.5 py-2 text-left text-sm text-popover-foreground outline-none transition-colors hover:bg-accent hover: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 badgeBase =\n  \"ml-auto inline-flex shrink-0 items-center rounded-full border border-[color-mix(in_oklab,var(--color-border)_80%,transparent)] bg-secondary px-2 py-0.5 text-xs font-medium text-secondary-foreground\"\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/* Deterministic initials from the label (nothing random). */\nfunction initialsOf(option: StyledRichOption) {\n  if (option.initials) return option.initials\n  return option.label\n    .split(\" \")\n    .filter(Boolean)\n    .slice(0, 2)\n    .map((w) => w[0]?.toUpperCase() ?? \"\")\n    .join(\"\")\n}\n\n/* A token-coloured inline SVG media square. No remote URLs. */\nfunction MediaTile({ seed }: { seed: number }) {\n  const bars = [6, 11, 8, 13]\n  return (\n    <span\n      aria-hidden=\"true\"\n      className=\"flex size-8 shrink-0 items-center justify-center rounded-md border border-[color-mix(in_oklab,var(--color-border)_70%,transparent)] bg-[color-mix(in_oklab,var(--color-primary)_12%,transparent)] text-primary\"\n    >\n      <svg viewBox=\"0 0 20 20\" className=\"size-4\" fill=\"none\" aria-hidden=\"true\">\n        {bars.map((h, i) => (\n          <rect\n            key={i}\n            x={2 + i * 4.5}\n            y={17 - ((h + seed * 2) % 12) - 3}\n            width=\"3\"\n            height={((h + seed * 2) % 12) + 3}\n            rx=\"1\"\n            fill=\"currentColor\"\n          />\n        ))}\n      </svg>\n    </span>\n  )\n}\n\ntype RichKind = \"avatar\" | \"description\" | \"badge\" | \"media\" | \"split\"\n\nfunction BaseRichSelect({\n  className,\n  size = \"md\",\n  placeholder = \"Select...\",\n  options,\n  value,\n  defaultValue,\n  onValueChange,\n  kind,\n}: StyledSelectProps & { kind: RichKind }) {\n  const list = options ?? defaultOptions\n  const reduce = useReducedMotion()\n  const uid = React.useId()\n  const listId = `${uid}-listbox`\n  const optionId = (i: number) => `${uid}-option-${i}`\n\n  const [open, setOpen] = React.useState(false)\n  const [active, setActive] = React.useState(0)\n  const [internal, setInternal] = React.useState<string | undefined>(defaultValue)\n  const wrapperRef = React.useRef<HTMLDivElement>(null)\n  const triggerRef = React.useRef<HTMLButtonElement>(null)\n\n  const isControlled = value !== undefined\n  const current = isControlled ? value : internal\n  const selected = list.find((o) => o.value === current)\n\n  const close = React.useCallback(() => {\n    setOpen(false)\n    triggerRef.current?.focus()\n  }, [])\n\n  const select = React.useCallback(\n    (next: string) => {\n      if (!isControlled) setInternal(next)\n      onValueChange?.(next)\n      setOpen(false)\n      triggerRef.current?.focus()\n    },\n    [isControlled, onValueChange],\n  )\n\n  React.useEffect(() => {\n    if (!open) return\n    const onPointer = (e: PointerEvent) => {\n      const node = wrapperRef.current\n      if (node && e.target instanceof Node && !node.contains(e.target)) setOpen(false)\n    }\n    window.addEventListener(\"pointerdown\", onPointer)\n    return () => window.removeEventListener(\"pointerdown\", onPointer)\n  }, [open])\n\n  const indexOfCurrent = () => {\n    const i = list.findIndex((o) => o.value === current)\n    return i < 0 ? 0 : i\n  }\n\n  const onKeyDown = (e: React.KeyboardEvent) => {\n    if (e.key === \"Escape\") {\n      if (!open) return\n      e.preventDefault()\n      close()\n      return\n    }\n    if (e.key === \"ArrowDown\" || e.key === \"ArrowUp\") {\n      e.preventDefault()\n      if (!open) {\n        setOpen(true)\n        setActive(indexOfCurrent())\n        return\n      }\n      const dir = e.key === \"ArrowDown\" ? 1 : -1\n      setActive((i) => (i + dir + list.length) % list.length)\n      return\n    }\n    if (e.key === \"Enter\" || e.key === \" \") {\n      e.preventDefault()\n      if (!open) {\n        setOpen(true)\n        setActive(indexOfCurrent())\n        return\n      }\n      const opt = list[active]\n      if (opt) select(opt.value)\n    }\n  }\n\n  const fade = { initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 } }\n  const anim = reduce ? fade : panelMotion\n\n  const triggerContent = (\n    <span className=\"flex min-w-0 items-center gap-2\">\n      {kind === \"avatar\" && selected ? (\n        <span\n          aria-hidden=\"true\"\n          className=\"flex size-5 shrink-0 items-center justify-center rounded-full bg-[color-mix(in_oklab,var(--color-primary)_18%,transparent)] text-[10px] font-semibold text-primary\"\n        >\n          {initialsOf(selected)}\n        </span>\n      ) : null}\n      <span className={cn(\"truncate\", selected === undefined && \"text-muted-foreground\")}>\n        {selected?.label ?? placeholder}\n      </span>\n      {kind === \"split\" && selected?.meta ? (\n        <span className=\"shrink-0 text-xs text-muted-foreground\">{selected.meta}</span>\n      ) : null}\n    </span>\n  )\n\n  return (\n    <div\n      ref={wrapperRef}\n      data-slot=\"styled-select\"\n      className={cn(\"relative inline-flex w-64 max-w-full\", panelWidth[size], className)}\n    >\n      <button\n        ref={triggerRef}\n        type=\"button\"\n        data-slot=\"styled-select-trigger\"\n        role=\"combobox\"\n        aria-expanded={open}\n        aria-haspopup=\"listbox\"\n        aria-controls={listId}\n        aria-activedescendant={open ? optionId(active) : undefined}\n        className={cn(triggerBase, triggerHeight[size])}\n        onClick={() => {\n          setActive(indexOfCurrent())\n          setOpen(!open)\n        }}\n        onKeyDown={onKeyDown}\n      >\n        {triggerContent}\n        <ChevronDown\n          className={cn(\"shrink-0 opacity-70 transition-transform duration-200\", open && \"rotate-180\")}\n        />\n      </button>\n\n      <AnimatePresence>\n        {open ? (\n          <motion.div\n            id={listId}\n            role=\"listbox\"\n            data-slot=\"styled-select-content\"\n            className={cn(panelBase, panelWidth[size])}\n            initial={anim.initial}\n            animate={anim.animate}\n            exit={anim.exit}\n            transition={reduce ? { duration: 0.14 } : panelMotion.transition}\n          >\n            <div className=\"flex flex-col\">\n              {list.map((option, i) => {\n                const isSelected = option.value === current\n                return (\n                  <button\n                    key={option.value}\n                    id={optionId(i)}\n                    type=\"button\"\n                    role=\"option\"\n                    tabIndex={-1}\n                    aria-selected={isSelected}\n                    className={cn(optionBase, i === active && \"bg-accent text-accent-foreground\")}\n                    onPointerEnter={() => setActive(i)}\n                    onClick={() => select(option.value)}\n                  >\n                    {kind === \"avatar\" ? (\n                      <span\n                        aria-hidden=\"true\"\n                        className=\"flex size-8 shrink-0 items-center justify-center rounded-full bg-[color-mix(in_oklab,var(--color-primary)_18%,transparent)] text-xs font-semibold text-primary\"\n                      >\n                        {initialsOf(option)}\n                      </span>\n                    ) : null}\n                    {kind === \"media\" ? <MediaTile seed={i} /> : null}\n\n                    <span className=\"flex min-w-0 flex-1 flex-col\">\n                      <span className=\"truncate font-medium\">{option.label}</span>\n                      {(kind === \"description\" || kind === \"avatar\" || kind === \"media\") &&\n                      option.description ? (\n                        <span className=\"truncate text-xs text-muted-foreground\">\n                          {option.description}\n                        </span>\n                      ) : null}\n                    </span>\n\n                    {kind === \"badge\" && option.badge ? (\n                      <span className={badgeBase}>{option.badge}</span>\n                    ) : null}\n                    {kind === \"split\" && option.meta ? (\n                      <span className=\"ml-auto shrink-0 text-xs text-muted-foreground\">\n                        {option.meta}\n                      </span>\n                    ) : null}\n\n                    <span className=\"flex size-4 shrink-0 items-center justify-center text-primary\">\n                      {isSelected ? <Check /> : null}\n                    </span>\n                  </button>\n                )\n              })}\n            </div>\n          </motion.div>\n        ) : null}\n      </AnimatePresence>\n    </div>\n  )\n}\n\n/* Avatar: her satirda token yuzeyli, bas harfli yuvarlak avatar. */\nexport function AvatarSelect(props: StyledSelectProps) {\n  return <BaseRichSelect {...props} kind=\"avatar\" />\n}\n\n/* Description: a second description line under the label. */\nexport function DescriptionSelect(props: StyledSelectProps) {\n  return <BaseRichSelect {...props} kind=\"description\" />\n}\n\n/* Badge: satirin sagina rol/durum rozeti dusar. */\nexport function BadgeSelect(props: StyledSelectProps) {\n  return <BaseRichSelect {...props} kind=\"badge\" />\n}\n\n/* Media: a media square carrying an inline SVG glyph at the start of the row. */\nexport function MediaSelect(props: StyledSelectProps) {\n  return <BaseRichSelect {...props} kind=\"media\" />\n}\n\n/* Split: label solda, meta metni sagda; trigger da bolunmus okunur. */\nexport function SplitSelect(props: StyledSelectProps) {\n  return <BaseRichSelect {...props} kind=\"split\" />\n}\n",
      "type": "registry:component",
      "target": "components/ui/select-rich.tsx"
    }
  ],
  "categories": [
    "styled",
    "select"
  ],
  "type": "registry:component"
}