{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "select",
  "title": "Select",
  "description": "Picks one option from a list, on an input's surface, with groups and scrolling.",
  "dependencies": [
    "@base-ui/react",
    "@hugeicons/core-free-icons",
    "@hugeicons/react",
    "@stylexjs/stylex"
  ],
  "registryDependencies": [
    "https://constella.aayush.cv/r/base.json"
  ],
  "files": [
    {
      "path": "src/components/primitives/select.tsx",
      "content": "\"use client\";\n\nimport { Select as SelectPrimitive } from \"@base-ui/react/select\";\nimport {\n  ArrowDown01Icon,\n  ArrowUp01Icon,\n  Tick02Icon,\n  UnfoldMoreIcon,\n} from \"@hugeicons/core-free-icons\";\nimport { HugeiconsIcon } from \"@hugeicons/react\";\nimport * as stylex from \"@stylexjs/stylex\";\nimport { createContext, use, useContext } from \"react\";\n\nimport { useSkipMotion } from \"@/lib/constella/motion\";\nimport type { Sx } from \"@/lib/constella/styles\";\nimport {\n  controlIconSize,\n  joinStyles,\n  stateStyles,\n  typeStyles,\n} from \"@/lib/constella/styles\";\nimport {\n  colors,\n  durations,\n  easings,\n  fontWeights,\n  layers,\n  media,\n  motion,\n  offsets,\n  opacities,\n  radii,\n  shadows,\n  sizes,\n  space,\n  strokes,\n} from \"@/lib/constella/tokens.stylex\";\n\nconst selectSizes = [\"sm\", \"default\", \"lg\"] as const;\n\ntype SelectSize = (typeof selectSizes)[number];\n\ntype Styled<T> = Omit<T, \"className\" | \"style\"> & {\n  sx?: Sx;\n};\n\ntype SelectTriggerProps = Styled<SelectPrimitive.Trigger.Props> & {\n  size?: SelectSize;\n};\n\ntype SelectContentProps = Styled<SelectPrimitive.Popup.Props> &\n  Pick<\n    SelectPrimitive.Positioner.Props,\n    \"align\" | \"alignItemWithTrigger\" | \"alignOffset\" | \"side\" | \"sideOffset\"\n  > & { size?: SelectSize };\n\nconst invalid = \":is([aria-invalid='true'], [data-invalid])\";\nconst disabled = \":is(:disabled, [data-disabled])\";\nconst offstage = \":is([data-starting-style], [data-ending-style])\";\n// Over the trigger, the popup opens in place, so it doesn't travel.\nconst aligned = \":is([data-side='none'])\";\nconst closing =\n  \":is([data-ending-style]):not([data-side='none'], [data-skip-motion])\";\nconst instant = \":is([data-skip-motion])\";\n\n// Base UI grows an aligned popup as it scrolls, up to the popup's `max-height`.\nconst maxHeight = `min(var(--available-height), ${sizes.menuHeight})`;\n\nconst styles = stylex.create({\n  // An input's surface, so a select and an input in one form read as one family.\n  trigger: {\n    alignItems: \"center\",\n    backgroundClip: \"padding-box\",\n    borderBlockColor: {\n      default: colors.border,\n      [invalid]: colors.dangerBorder,\n    },\n    borderInlineColor: {\n      default: colors.border,\n      [invalid]: colors.dangerBorder,\n    },\n    borderBlockEndWidth: strokes.border,\n    borderBlockStartWidth: strokes.border,\n    borderInlineEndWidth: strokes.border,\n    borderInlineStartWidth: strokes.border,\n    borderBlockStyle: \"solid\",\n    borderInlineStyle: \"solid\",\n    boxShadow: {\n      default: shadows.default,\n      [`:is([data-popup-open]):not(${invalid})`]: shadows.defaultHover,\n      // Repeated inside the media block, which StyleX ranks above the bare conditions.\n      [media.hover]: {\n        default: shadows.default,\n        [`:is([data-popup-open]):not(${invalid})`]: shadows.defaultHover,\n        [`:hover:not(:disabled, :active):not(${invalid})`]:\n          shadows.defaultHover,\n        [`:active:not(:disabled):not(${invalid})`]: shadows.defaultPressed,\n        [invalid]: shadows.defaultInvalid,\n      },\n      [`:active:not(:disabled):not(${invalid})`]: shadows.defaultPressed,\n      [invalid]: shadows.defaultInvalid,\n    },\n    color: colors.textPrimary,\n    cursor: { default: \"pointer\", [disabled]: \"not-allowed\" },\n    display: \"flex\",\n    gap: space.row,\n    height: sizes.controlMd,\n    justifyContent: \"space-between\",\n    marginBlock: 0,\n    marginInline: 0,\n    maxWidth: \"100%\",\n    minWidth: 0,\n    opacity: { default: 1, [disabled]: opacities.disabled },\n    paddingInlineEnd: space.xs,\n    paddingInlineStart: space.popup,\n    transitionDuration: durations.hover,\n    transitionProperty: \"background-color, border-color, box-shadow\",\n    transitionTimingFunction: \"ease\",\n    userSelect: \"none\",\n    whiteSpace: \"nowrap\",\n    width: \"fit-content\",\n    // In a ButtonGroup, the focused item's ring stays above its neighbors.\n    zIndex: { default: null, \":focus-visible\": 1 },\n  },\n  sm: {\n    height: sizes.controlSm,\n  },\n  lg: { height: sizes.controlLg },\n  // A block, not a flex box: an ellipsis only ends text in a block.\n  value: {\n    color: { default: null, \":is([data-placeholder] *)\": colors.textMuted },\n    display: \"block\",\n    flex: 1,\n    minWidth: 0,\n    overflow: \"hidden\",\n    textAlign: \"start\",\n    textOverflow: \"ellipsis\",\n  },\n  icon: {\n    color: colors.textMuted,\n    display: \"flex\",\n    flexShrink: 0,\n  },\n  positioner: {\n    zIndex: layers.popover,\n  },\n  popup: {\n    backgroundColor: colors.raised,\n    borderStartStartRadius: radii.lg,\n    borderStartEndRadius: radii.lg,\n    borderEndStartRadius: radii.lg,\n    borderEndEndRadius: radii.lg,\n    boxShadow: shadows.lg,\n    color: colors.textPrimary,\n    maxHeight,\n    maxWidth: \"var(--available-width)\",\n    minWidth: sizes.menu,\n    opacity: { default: 1, [offstage]: 0 },\n    transform: { default: \"none\", [offstage]: `scale(${motion.popoverScale})` },\n    // Holds the scroll arrows, which Base UI positions absolutely.\n    position: \"relative\",\n    transformOrigin: \"var(--transform-origin)\",\n    transitionDuration: {\n      default: durations.popover,\n      [closing]: durations.popoverExit,\n      [aligned]: \"0s\",\n      [instant]: \"0s\",\n    },\n    transitionProperty: {\n      default: \"opacity, transform\",\n      [media.reducedMotion]: \"opacity\",\n    },\n    transitionTimingFunction: easings.out,\n    width: \"var(--anchor-width)\",\n  },\n  list: {\n    maxHeight,\n    overflowX: \"hidden\",\n    overflowY: \"auto\",\n    paddingBlock: space.xxs,\n    paddingInlineEnd: space.xxs,\n    paddingInlineStart: space.xxs,\n    position: \"relative\",\n    scrollPaddingBlock: sizes.controlXs,\n  },\n  item: {\n    alignItems: \"center\",\n    backgroundColor: {\n      default: \"transparent\",\n      \":is([data-highlighted])\": colors.fillSubtle,\n    },\n    borderStartStartRadius: radii.md,\n    borderStartEndRadius: radii.md,\n    borderEndStartRadius: radii.md,\n    borderEndEndRadius: radii.md,\n    cursor: { default: \"pointer\", \":is([data-disabled])\": \"not-allowed\" },\n    display: \"flex\",\n    gap: space.row,\n    minHeight: sizes.controlMd,\n    opacity: { default: 1, \":is([data-disabled])\": opacities.disabled },\n    paddingInlineEnd: `calc(${sizes.icon} + ${space.sm})`,\n    paddingInlineStart: space.sm,\n    position: \"relative\",\n    userSelect: \"none\",\n  },\n  itemSm: {\n    minHeight: sizes.controlSm,\n    paddingInlineEnd: `calc(${sizes.icon} + ${space.row} * 2)`,\n    paddingInlineStart: space.row,\n  },\n  itemLg: {\n    minHeight: sizes.controlLg,\n    paddingInlineEnd: `calc(${sizes.icon} + ${space.md})`,\n    paddingInlineStart: space.md,\n  },\n  // Wraps rather than truncating, so options that differ only at the end stay apart.\n  itemText: {\n    alignItems: \"center\",\n    display: \"flex\",\n    flex: 1,\n    gap: space.xs,\n    minWidth: 0,\n    overflowWrap: \"anywhere\",\n  },\n  indicator: {\n    alignItems: \"center\",\n    display: \"flex\",\n    insetInlineEnd: space.sm,\n    justifyContent: \"center\",\n    position: \"absolute\",\n  },\n  indicatorSm: { insetInlineEnd: space.row },\n  indicatorLg: { insetInlineEnd: space.md },\n  label: {\n    color: colors.textMuted,\n    fontWeight: fontWeights.medium,\n    paddingBlock: space.xxs,\n    paddingInlineEnd: space.sm,\n    paddingInlineStart: space.sm,\n  },\n  labelSm: { paddingInlineEnd: space.row, paddingInlineStart: space.row },\n  labelLg: { paddingInlineEnd: space.md, paddingInlineStart: space.md },\n  // In line with the rows' text.\n  separator: {\n    backgroundColor: colors.borderSubtle,\n    height: strokes.border,\n    marginBlock: space.xxs,\n    marginInline: space.sm,\n  },\n  separatorSm: { marginInline: space.row },\n  separatorLg: { marginInline: space.md },\n  scrollArrow: {\n    alignItems: \"center\",\n    backgroundColor: colors.raised,\n    borderStartStartRadius: radii.md,\n    borderStartEndRadius: radii.md,\n    borderEndStartRadius: radii.md,\n    borderEndEndRadius: radii.md,\n    insetBlockEnd: { default: null, \":is([data-direction='down'])\": 0 },\n    insetBlockStart: { default: null, \":is([data-direction='up'])\": 0 },\n    color: colors.textMuted,\n    cursor: \"default\",\n    display: \"flex\",\n    height: sizes.controlXs,\n    justifyContent: \"center\",\n    width: \"100%\",\n    zIndex: 1,\n  },\n});\n\nconst sizeStyles = {\n  default: null,\n  lg: styles.lg,\n  sm: [typeStyles.captionLg, styles.sm],\n} satisfies Record<SelectSize, stylex.StyleXStyles | null>;\n\nfunction Glyph({\n  icon,\n  size = \"default\",\n}: {\n  icon: typeof Tick02Icon;\n  size?: SelectSize;\n}) {\n  return (\n    <HugeiconsIcon\n      aria-hidden\n      icon={icon}\n      size={controlIconSize[size]}\n      strokeWidth={Number(strokes.icon)}\n    />\n  );\n}\n\n// The popup's rows by `SelectContent`'s size; the base styles are `default`'s.\nconst contentSizeStyles = {\n  default: {\n    indicator: null,\n    item: null,\n    label: null,\n    separator: null,\n    text: typeStyles.textMd,\n  },\n  lg: {\n    indicator: styles.indicatorLg,\n    item: styles.itemLg,\n    label: styles.labelLg,\n    separator: styles.separatorLg,\n    text: typeStyles.textBase,\n  },\n  sm: {\n    indicator: styles.indicatorSm,\n    item: styles.itemSm,\n    label: styles.labelSm,\n    separator: styles.separatorSm,\n    text: typeStyles.textSm,\n  },\n} satisfies Record<\n  SelectSize,\n  Record<\n    \"indicator\" | \"item\" | \"label\" | \"separator\" | \"text\",\n    stylex.StyleXStyles | null\n  >\n>;\n\nconst SkipMotion = createContext(false);\nconst Size = createContext<SelectSize>(\"default\");\n\nfunction Select<Value, Multiple extends boolean | undefined = false>({\n  onOpenChange,\n  ...props\n}: SelectPrimitive.Root.Props<Value, Multiple>) {\n  const [skip, handleOpenChange] = useSkipMotion(onOpenChange);\n  return (\n    <SkipMotion value={skip}>\n      <SelectPrimitive.Root onOpenChange={handleOpenChange} {...props} />\n    </SkipMotion>\n  );\n}\n\nfunction SelectGroup({ sx, ...props }: Styled<SelectPrimitive.Group.Props>) {\n  return (\n    <SelectPrimitive.Group\n      data-slot=\"select-group\"\n      {...props}\n      {...stylex.props(sx)}\n    />\n  );\n}\n\nfunction SelectValue({ sx, ...props }: Styled<SelectPrimitive.Value.Props>) {\n  return (\n    <SelectPrimitive.Value\n      data-slot=\"select-value\"\n      {...props}\n      {...stylex.props(styles.value, sx)}\n    />\n  );\n}\n\nfunction SelectTrigger({\n  children,\n  size = \"default\",\n  sx,\n  ...props\n}: SelectTriggerProps) {\n  return (\n    <SelectPrimitive.Trigger\n      data-size={size}\n      data-slot=\"select-trigger\"\n      {...props}\n      {...stylex.props(\n        [typeStyles.textSm, styles.trigger],\n        stateStyles.outline,\n        joinStyles.md,\n        joinStyles.edges,\n        sizeStyles[size],\n        sx\n      )}\n    >\n      {children}\n      <SelectPrimitive.Icon {...stylex.props(styles.icon)}>\n        <Glyph icon={UnfoldMoreIcon} size={size} />\n      </SelectPrimitive.Icon>\n    </SelectPrimitive.Trigger>\n  );\n}\n\nfunction SelectScrollUpButton({\n  sx,\n  ...props\n}: Styled<SelectPrimitive.ScrollUpArrow.Props>) {\n  return (\n    <SelectPrimitive.ScrollUpArrow\n      data-slot=\"select-scroll-up-button\"\n      {...props}\n      {...stylex.props(styles.scrollArrow, sx)}\n    >\n      <Glyph icon={ArrowUp01Icon} />\n    </SelectPrimitive.ScrollUpArrow>\n  );\n}\n\nfunction SelectScrollDownButton({\n  sx,\n  ...props\n}: Styled<SelectPrimitive.ScrollDownArrow.Props>) {\n  return (\n    <SelectPrimitive.ScrollDownArrow\n      data-slot=\"select-scroll-down-button\"\n      {...props}\n      {...stylex.props(styles.scrollArrow, sx)}\n    >\n      <Glyph icon={ArrowDown01Icon} />\n    </SelectPrimitive.ScrollDownArrow>\n  );\n}\n\n/** Renders the portal, the positioner and the popup. Placement props go to the positioner. */\nfunction SelectContent({\n  align = \"start\",\n  alignItemWithTrigger = false,\n  alignOffset = 0,\n  children,\n  side = \"bottom\",\n  sideOffset = Number(offsets.popover),\n  size = \"default\",\n  sx,\n  ...props\n}: SelectContentProps) {\n  const skip = useContext(SkipMotion);\n  return (\n    <Size value={size}>\n      <SelectPrimitive.Portal>\n        <SelectPrimitive.Positioner\n          align={align}\n          alignItemWithTrigger={alignItemWithTrigger}\n          alignOffset={alignOffset}\n          side={side}\n          sideOffset={sideOffset}\n          {...stylex.props(styles.positioner)}\n        >\n          <SelectPrimitive.Popup\n            data-size={size}\n            data-skip-motion={skip || undefined}\n            data-slot=\"select-content\"\n            {...props}\n            {...stylex.props(styles.popup, sx)}\n          >\n            <SelectScrollUpButton />\n            <SelectPrimitive.List {...stylex.props(styles.list)}>\n              {children}\n            </SelectPrimitive.List>\n            <SelectScrollDownButton />\n          </SelectPrimitive.Popup>\n        </SelectPrimitive.Positioner>\n      </SelectPrimitive.Portal>\n    </Size>\n  );\n}\n\nfunction SelectLabel({\n  sx,\n  ...props\n}: Styled<SelectPrimitive.GroupLabel.Props>) {\n  return (\n    <SelectPrimitive.GroupLabel\n      data-slot=\"select-label\"\n      {...props}\n      {...stylex.props(\n        [typeStyles.captionLg, styles.label],\n        contentSizeStyles[use(Size)].label,\n        sx\n      )}\n    />\n  );\n}\n\nfunction SelectItem({\n  children,\n  sx,\n  ...props\n}: Styled<SelectPrimitive.Item.Props>) {\n  const size = use(Size);\n  const sized = contentSizeStyles[size];\n  return (\n    <SelectPrimitive.Item\n      data-slot=\"select-item\"\n      {...props}\n      {...stylex.props([sized.text, styles.item], sized.item, sx)}\n    >\n      <SelectPrimitive.ItemText {...stylex.props(styles.itemText)}>\n        {children}\n      </SelectPrimitive.ItemText>\n      <SelectPrimitive.ItemIndicator\n        {...stylex.props(styles.indicator, sized.indicator)}\n      >\n        <Glyph icon={Tick02Icon} size={size} />\n      </SelectPrimitive.ItemIndicator>\n    </SelectPrimitive.Item>\n  );\n}\n\nfunction SelectSeparator({\n  sx,\n  ...props\n}: Styled<SelectPrimitive.Separator.Props>) {\n  return (\n    <SelectPrimitive.Separator\n      data-slot=\"select-separator\"\n      {...props}\n      {...stylex.props(\n        styles.separator,\n        contentSizeStyles[use(Size)].separator,\n        sx\n      )}\n    />\n  );\n}\n\nexport {\n  Select,\n  SelectContent,\n  SelectGroup,\n  SelectItem,\n  SelectLabel,\n  SelectScrollDownButton,\n  SelectScrollUpButton,\n  SelectSeparator,\n  selectSizes,\n  SelectTrigger,\n  SelectValue,\n};\nexport type { SelectContentProps, SelectSize, SelectTriggerProps };\n",
      "type": "registry:component"
    }
  ],
  "docs": "Usage: https://constella.aayush.cv/docs/components/select/index.md",
  "type": "registry:ui"
}