{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "select",
  "type": "registry:ui",
  "title": "Select",
  "description": "Opens over its trigger with the choice aligned, and the new value slides in from where it was picked.",
  "categories": [
    "pickers"
  ],
  "dependencies": [
    "@base-ui/react",
    "motion"
  ],
  "registryDependencies": [
    "https://www.stealth.pm/r/tokens.json",
    "https://www.stealth.pm/r/cn.json",
    "https://www.stealth.pm/r/icons.json",
    "https://www.stealth.pm/r/motion.json"
  ],
  "files": [
    {
      "path": "components/ui/select.tsx",
      "content": "\"use client\";\nimport { Select as SelectPrimitive } from \"@base-ui/react/select\";\nimport { AnimatePresence, animate, motion, useMotionValue, useReducedMotion, type AnimationPlaybackControls } from \"motion/react\";\nimport { createContext, use, useEffect, useMemo, useState } from \"react\";\nimport { cn } from \"@/lib/cn\";\nimport { Check, ChevronDown, ChevronUp, ChevronsUpDown } from \"@/lib/icons\";\nimport { ease, spring } from \"@/lib/motion\";\n\ntype Size = \"sm\" | \"md\" | \"lg\";\n// Base UI accepts a className function; these parts take a plain string so cn() can merge it.\ntype Styled<P> = Omit<P, \"className\"> & { className?: string };\ntype Items = SelectPrimitive.Root.Props<unknown>[\"items\"];\n\ntype Ctx = { value: unknown; indexOf: (value: unknown) => number };\nconst SelectContext = createContext<Ctx>({ value: null, indexOf: () => -1 });\n\n// Flattens the `items` prop (record, flat array or groups) into one ordered list of values,\n// so the trigger knows whether a new choice sits above or below the old one.\nfunction flatten(items: Items): unknown[] {\n  if (!items) return [];\n  if (!Array.isArray(items)) return Object.keys(items);\n  return items.flatMap((entry) => (\"items\" in entry && Array.isArray(entry.items) ? entry.items.map((i: { value: unknown }) => i.value) : [entry.value]));\n}\n\nexport type SelectProps<Value> = Omit<SelectPrimitive.Root.Props<Value, false>, \"multiple\">;\n\n/** The root. Pass `items` so the trigger can show labels and animate in the right direction. */\nexport function Select<Value>({ value, defaultValue, onValueChange, items, ...rest }: SelectProps<Value>) {\n  const [inner, setInner] = useState<Value | null>(defaultValue ?? null);\n  const controlled = value !== undefined;\n  const current = controlled ? value : inner;\n  const order = useMemo(() => flatten(items), [items]);\n  const ctx = useMemo<Ctx>(() => ({ value: current, indexOf: (v) => order.indexOf(v) }), [current, order]);\n\n  return (\n    <SelectContext value={ctx}>\n      <SelectPrimitive.Root<Value, false>\n        items={items}\n        value={current}\n        onValueChange={(next, details) => {\n          if (!controlled) setInner(next);\n          onValueChange?.(next, details);\n        }}\n        {...rest}\n      />\n    </SelectContext>\n  );\n}\n\nexport type SelectLabelProps = Styled<SelectPrimitive.Label.Props>;\n\nexport function SelectLabel({ className, ...rest }: SelectLabelProps) {\n  return <SelectPrimitive.Label className={cn(\"w-fit cursor-default text-[12.5px] font-medium text-fg-2 select-none\", className)} {...rest} />;\n}\n\nexport type SelectTriggerProps = Omit<Styled<SelectPrimitive.Trigger.Props>, \"children\"> & {\n  placeholder?: React.ReactNode;\n  /** Decorative icon before the value. */\n  icon?: React.ReactNode;\n  size?: Size;\n  /** Marks the field as invalid when it isn't inside a Field that does it for you. */\n  invalid?: boolean;\n  /** Render the chosen value yourself. Receives the raw value. */\n  children?: (value: unknown) => React.ReactNode;\n};\n\nexport function SelectTrigger({ placeholder = \"Select…\", icon, size = \"md\", invalid, className, children, ...rest }: SelectTriggerProps) {\n  const { value, indexOf } = use(SelectContext);\n  const reduce = useReducedMotion();\n  // The new value arrives from the side of the list it was picked from: below slides up, above slides down.\n  const [shown, setShown] = useState({ value, dir: 1 });\n  if (!Object.is(shown.value, value)) setShown({ value, dir: indexOf(value) < indexOf(shown.value) ? -1 : 1 });\n  const dir = shown.dir;\n\n  return (\n    <SelectPrimitive.Trigger\n      data-size={size}\n      aria-invalid={invalid || undefined}\n      className={cn(\n        \"group/trigger relative flex w-full min-w-0 items-center border border-line-2 bg-raised text-left text-fg shadow-[var(--shadow)] select-none\",\n        \"outline-none focus-visible:outline-solid focus-visible:outline-1 focus-visible:outline-offset-2 focus-visible:outline-fg-3\",\n        \"transition-[background-color,border-color,color,scale] duration-150 ease-out active:scale-[0.985] active:duration-75\",\n        \"hover:border-fg-4 hover:bg-hover data-popup-open:border-fg-4 data-popup-open:bg-hover\",\n        \"aria-invalid:border-danger/70 hover:aria-invalid:border-danger data-invalid:border-danger/70\",\n        \"data-disabled:pointer-events-none data-disabled:opacity-50\",\n        \"data-readonly:bg-transparent data-readonly:shadow-none\",\n        // A 44px touch target without changing how the field looks.\n        \"pointer-coarse:after:absolute pointer-coarse:after:inset-x-0 pointer-coarse:after:-inset-y-1.5\",\n        size === \"sm\" && \"h-7 gap-1.5 rounded-md pr-1.5 pl-2 text-[12.5px]\",\n        size === \"md\" && \"h-8 gap-2 rounded-lg pr-2 pl-2.5 text-[13px]\",\n        size === \"lg\" && \"h-9 gap-2 rounded-lg pr-2.5 pl-3 text-[13px]\",\n        className,\n      )}\n      {...rest}\n    >\n      {icon && <span className=\"flex shrink-0 text-fg-3 [&_svg]:size-3.5\">{icon}</span>}\n      <SelectPrimitive.Value\n        placeholder={placeholder}\n        render={(props, state) => (\n          <span {...props} className=\"grid min-w-0 flex-1 grid-cols-[minmax(0,1fr)] overflow-hidden py-1\">\n            <AnimatePresence initial={false} custom={dir}>\n              <motion.span\n                key={String(state.placeholder ? \"\" : keyOf(state.value))}\n                custom={dir}\n                className={cn(\"col-start-1 row-start-1 truncate\", state.placeholder && \"text-fg-4\")}\n                variants={{\n                  enter: (d: number) => (reduce ? { opacity: 0 } : { opacity: 0, y: 8 * d, filter: \"blur(2px)\" }),\n                  center: { opacity: 1, y: 0, filter: \"blur(0px)\" },\n                  exit: (d: number) => (reduce ? { opacity: 0 } : { opacity: 0, y: -8 * d, filter: \"blur(2px)\" }),\n                }}\n                initial=\"enter\"\n                animate=\"center\"\n                exit=\"exit\"\n                transition={{ duration: reduce ? 0.12 : 0.22, ease: ease.out }}\n              >\n                {children && !state.placeholder ? children(state.value) : props.children}\n              </motion.span>\n            </AnimatePresence>\n          </span>\n        )}\n      />\n      <SelectPrimitive.Icon\n        className={cn(\n          \"flex shrink-0 text-fg-3 transition-colors duration-150 group-hover/trigger:text-fg-2 group-data-popup-open/trigger:text-fg-2 group-data-readonly/trigger:hidden\",\n          size === \"sm\" ? \"[&_svg]:size-3.5\" : \"[&_svg]:size-4\",\n        )}\n      >\n        <ChevronsUpDown />\n      </SelectPrimitive.Icon>\n    </SelectPrimitive.Trigger>\n  );\n}\n\nfunction keyOf(value: unknown): string {\n  if (value && typeof value === \"object\") {\n    const v = value as { value?: unknown; id?: unknown };\n    return String(v.value ?? v.id ?? JSON.stringify(value));\n  }\n  return String(value);\n}\n\nexport type SelectPopupProps = Styled<SelectPrimitive.Popup.Props> & {\n  /** Overlap the trigger so the chosen item sits exactly over the value. Falls back to below when there isn't room, and is always off for touch. */\n  alignItemWithTrigger?: boolean;\n  side?: SelectPrimitive.Positioner.Props[\"side\"];\n  align?: SelectPrimitive.Positioner.Props[\"align\"];\n  sideOffset?: number;\n  /** Portal target. Defaults to document.body. */\n  container?: SelectPrimitive.Portal.Props[\"container\"];\n};\n\nexport function SelectPopup({ alignItemWithTrigger = true, side = \"bottom\", align = \"start\", sideOffset = 6, container, className, children, ...rest }: SelectPopupProps) {\n  const reduce = !!useReducedMotion();\n  const { setList, y, height, opacity } = useGlide(reduce);\n\n  return (\n    <SelectPrimitive.Portal container={container}>\n      <SelectPrimitive.Positioner\n        alignItemWithTrigger={alignItemWithTrigger}\n        side={side}\n        align={align}\n        sideOffset={sideOffset}\n        collisionPadding={8}\n        className=\"z-(--z-popover) outline-none select-none\"\n      >\n        <SelectPrimitive.Popup\n          className={cn(\n            \"group/popup relative min-w-(--anchor-width) max-w-(--available-width) origin-(--transform-origin) overflow-hidden rounded-xl border border-line-2 bg-raised text-fg shadow-pop outline-none\",\n            \"transition-[opacity,scale] duration-160 ease-out-expo data-ending-style:duration-100 data-ending-style:ease-out\",\n            \"data-starting-style:scale-[0.96] data-starting-style:opacity-0 data-ending-style:scale-[0.98] data-ending-style:opacity-0\",\n            // Aligned over the trigger, Base UI measures the popup to line the item up; a scale would throw that off, so it only fades.\n            \"data-[side=none]:data-starting-style:scale-100 data-[side=none]:data-ending-style:scale-100 data-[side=none]:min-w-[calc(var(--anchor-width)+0.5rem)]\",\n            \"motion-reduce:data-starting-style:scale-100 motion-reduce:data-ending-style:scale-100\",\n            className,\n          )}\n          {...rest}\n        >\n          <ScrollArrow direction=\"up\" />\n          <SelectPrimitive.List\n            ref={setList}\n            className=\"relative max-h-[min(var(--available-height),19rem)] scroll-py-6 overflow-y-auto overscroll-contain p-1 outline-none\"\n          >\n            <motion.div aria-hidden style={{ y, height, opacity }} className=\"pointer-events-none absolute inset-x-1 top-0 rounded-lg bg-line\" />\n            {children}\n          </SelectPrimitive.List>\n          <ScrollArrow direction=\"down\" />\n        </SelectPrimitive.Popup>\n      </SelectPrimitive.Positioner>\n    </SelectPrimitive.Portal>\n  );\n}\n\nfunction ScrollArrow({ direction }: { direction: \"up\" | \"down\" }) {\n  const Part = direction === \"up\" ? SelectPrimitive.ScrollUpArrow : SelectPrimitive.ScrollDownArrow;\n  return (\n    <Part\n      className={cn(\n        \"absolute inset-x-0 z-[2] flex h-6 cursor-default items-center justify-center bg-raised text-fg-3 transition-opacity duration-150\",\n        \"data-starting-style:opacity-0 data-ending-style:opacity-0\",\n        // Where the pointer rests to keep scrolling, extended past the edge in aligned mode so it's easy to find.\n        \"before:absolute before:inset-x-0 before:h-full before:content-['']\",\n        direction === \"up\" ? \"top-0 rounded-t-xl data-[side=none]:before:-top-full\" : \"bottom-0 rounded-b-xl data-[side=none]:before:-bottom-full\",\n      )}\n    >\n      {direction === \"up\" ? <ChevronUp size={14} /> : <ChevronDown size={14} />}\n    </Part>\n  );\n}\n\n// One highlight for the whole list. It glides after the pointer on a spring and\n// jumps instantly for arrow keys, where motion would read as lag.\nfunction useGlide(reduce: boolean) {\n  const [list, setList] = useState<HTMLDivElement | null>(null);\n  const y = useMotionValue(0);\n  const height = useMotionValue(32);\n  const opacity = useMotionValue(0);\n\n  useEffect(() => {\n    if (!list) return;\n    let keyboard = false;\n    let shown = false;\n    let running: AnimationPlaybackControls[] = [];\n    const stop = () => {\n      running.forEach((c) => c.stop());\n      running = [];\n    };\n    const place = () => {\n      const el = list.querySelector<HTMLElement>(\"[data-highlighted]\");\n      stop();\n      if (!el) {\n        shown = false;\n        running.push(animate(opacity, 0, { duration: reduce ? 0 : 0.12 }));\n        return;\n      }\n      // offsetTop ignores the popup's entrance scale, which would skew a measured rect.\n      let top = 0;\n      // Disabled rows can be reached by arrow keys but read as unavailable, so the highlight dims.\n      const target = el.hasAttribute(\"data-disabled\") ? 0.45 : 1;\n      for (let n: HTMLElement | null = el; n && n !== list; n = n.offsetParent as HTMLElement | null) top += n.offsetTop;\n      if (!shown || keyboard || reduce) {\n        y.jump(top);\n        height.jump(el.offsetHeight);\n        if (keyboard || reduce) opacity.jump(target);\n        else running.push(animate(opacity, target, { duration: 0.08 }));\n      } else {\n        running.push(animate(y, top, spring.follow), animate(height, el.offsetHeight, spring.follow));\n        opacity.jump(target);\n      }\n      shown = true;\n    };\n    const onKey = () => (keyboard = true);\n    const onPointer = () => (keyboard = false);\n    const observer = new MutationObserver(place);\n    observer.observe(list, { subtree: true, attributes: true, attributeFilter: [\"data-highlighted\"] });\n    document.addEventListener(\"keydown\", onKey, true);\n    list.addEventListener(\"pointermove\", onPointer);\n    place();\n    return () => {\n      observer.disconnect();\n      document.removeEventListener(\"keydown\", onKey, true);\n      list.removeEventListener(\"pointermove\", onPointer);\n      stop();\n    };\n  }, [list, reduce, y, height, opacity]);\n\n  return { setList, y, height, opacity };\n}\n\nexport type SelectItemProps = Styled<SelectPrimitive.Item.Props> & {\n  /** Decorative icon before the label. */\n  icon?: React.ReactNode;\n  /** A second, quieter line under the label. */\n  description?: React.ReactNode;\n  /** Right-aligned meta: a code, a count, a shortcut. */\n  hint?: React.ReactNode;\n};\n\nexport function SelectItem({ icon, description, hint, className, children, ...rest }: SelectItemProps) {\n  return (\n    <SelectPrimitive.Item\n      className={cn(\n        \"relative z-[1] flex cursor-default scroll-my-1 items-center gap-2 rounded-lg pr-2 pl-2 text-[13px] text-fg outline-none select-none\",\n        description ? \"min-h-11 py-1.5\" : \"h-8 pointer-coarse:h-10\",\n        \"data-disabled:text-fg-4 data-disabled:[&_span]:text-fg-4\",\n        className,\n      )}\n      {...rest}\n    >\n      {icon && <span className=\"flex shrink-0 text-fg-3 [&_svg]:size-4\">{icon}</span>}\n      <span className=\"flex min-w-0 flex-1 flex-col\">\n        <SelectPrimitive.ItemText className=\"truncate\">{children}</SelectPrimitive.ItemText>\n        {description && <span className=\"truncate text-[12px] leading-4 text-fg-3\">{description}</span>}\n      </span>\n      {hint && <span className=\"shrink-0 font-mono text-2xs text-fg-4 tabular\">{hint}</span>}\n      {/* The check has its own column so hints never shift when the selection moves. */}\n      <span className=\"grid size-4 shrink-0 place-items-center\">\n        <SelectPrimitive.ItemIndicator>\n          <Check size={14} className=\"text-fg\" />\n        </SelectPrimitive.ItemIndicator>\n      </span>\n    </SelectPrimitive.Item>\n  );\n}\n\nexport type SelectGroupProps = Styled<SelectPrimitive.Group.Props> & { label?: React.ReactNode };\n\nexport function SelectGroup({ label, className, children, ...rest }: SelectGroupProps) {\n  return (\n    <SelectPrimitive.Group className={cn(\"pb-1 last:pb-0\", className)} {...rest}>\n      {label && (\n        <SelectPrimitive.GroupLabel className=\"px-2 pt-2 pb-1 font-mono text-2xs tracking-[0.08em] text-fg-3 uppercase select-none\">{label}</SelectPrimitive.GroupLabel>\n      )}\n      {children}\n    </SelectPrimitive.Group>\n  );\n}\n\nexport type SelectSeparatorProps = Styled<SelectPrimitive.Separator.Props>;\n\nexport function SelectSeparator({ className, ...rest }: SelectSeparatorProps) {\n  return <SelectPrimitive.Separator className={cn(\"-mx-1 my-1 h-px bg-line\", className)} {...rest} />;\n}\n",
      "type": "registry:ui"
    }
  ],
  "docs": "https://www.stealth.pm/ui/select"
}
