# Halo Select

> Composable select dropdown for React and shadcn/ui on Base UI, with a gradient rim, staggered label, and loading indicator.

Source: https://www.cult-ui.com/docs/components/halo-select

## Example

```tsx title="halo-select-demo.tsx"
"use client"

import { useState } from "react"

import { Button } from "@/components/ui/button"
import {
  HaloSelect,
  HaloSelectContent,
  HaloSelectItem,
  HaloSelectTrailingIndicator,
  HaloSelectTrigger,
  HaloSelectValue,
  type PolishedSelectItem,
} from "@/components/ui/halo-select"

const MOCK_ITEMS: PolishedSelectItem[] = [
  { value: "prod", label: "Production" },
  { value: "staging", label: "Staging" },
  { value: "dev", label: "Development" },
  { value: "preview", label: "Preview", disabled: true },
  { value: "edge", label: "Edge" },
]

const NO_ITEMS: PolishedSelectItem[] = []

/** How long the demo keeps `isLoading` true. */
const MOCK_LOADING_MS = 2000

function itemEqual(a: unknown, b: unknown) {
  return (
    (a as PolishedSelectItem | null | undefined)?.value ===
    (b as PolishedSelectItem | null | undefined)?.value
  )
}

export default function HaloSelectDemo() {
  const [value, setValue] = useState<PolishedSelectItem | null>(null)
  const [isLoading, setIsLoading] = useState(false)

  return (
    <main className="flex w-full flex-col items-center justify-center">
      <div className="w-full max-w-2xl space-y-10">
        <section aria-labelledby="demo-heading" className="space-y-3">
          <h2
            className="font-medium text-foreground text-sm tracking-tight"
            id="demo-heading"
          >
            Select
          </h2>
          <p className="text-muted-foreground text-sm">
            Opens a frosted listbox from a trigger: pick one option, show a
            loading state on the trigger, and flash the selection when it
            changes.
          </p>
          <HaloSelect
            isItemEqualToValue={itemEqual}
            isLoading={isLoading}
            items={MOCK_ITEMS}
            onValueChange={(next) =>
              setValue(next as PolishedSelectItem | null)
            }
            placeholder="Choose environment"
            showSelectionFlash
            translucent
            value={value}
          >
            <HaloSelectTrigger id="demo-halo-select">
              <HaloSelectValue />
              <HaloSelectTrailingIndicator />
            </HaloSelectTrigger>
            <HaloSelectContent>
              {MOCK_ITEMS.map((item, listIndex) => (
                <HaloSelectItem
                  disabled={item.disabled}
                  index={listIndex}
                  key={item.value}
                  label={item.label}
                  value={item}
                >
                  {item.label}
                </HaloSelectItem>
              ))}
            </HaloSelectContent>
          </HaloSelect>
        </section>

        <section aria-labelledby="empty-heading" className="space-y-3">
          <h2
            className="font-medium text-foreground text-sm tracking-tight"
            id="empty-heading"
          >
            Empty list
          </h2>
          <p className="text-muted-foreground text-sm">
            When the list is empty, show placeholder copy in the dropdown panel
            instead of options.
          </p>
          <HaloSelect
            isItemEqualToValue={itemEqual}
            items={NO_ITEMS}
            placeholder="No data"
            value={null}
          >
            <HaloSelectTrigger id="demo-empty-select">
              <HaloSelectValue />
              <HaloSelectTrailingIndicator />
            </HaloSelectTrigger>
            <HaloSelectContent>
              <p className="px-3 py-6 text-center text-muted-foreground text-sm">
                No options.
              </p>
            </HaloSelectContent>
          </HaloSelect>
        </section>

        <div className="flex flex-wrap gap-3 border-border border-t pt-8">
          <Button
            disabled={isLoading}
            onClick={() => {
              setIsLoading(true)
              window.setTimeout(() => setIsLoading(false), MOCK_LOADING_MS)
            }}
            type="button"
            variant="secondary"
          >
            Simulate load
          </Button>
          <Button
            onClick={() => setValue(null)}
            type="button"
            variant="outline"
          >
            Clear selection
          </Button>
        </div>
      </div>
    </main>
  )
}
```

Halo Select is a composable React select for shadcn/ui built on Base UI Select and Motion. Use it for dropdown choices in settings pages, filters, and model pickers in AI apps.

## Installation

### CLI

```bash
npx shadcn@latest add @cult-ui/halo-select
```

### Manual

**Install the required dependencies:**

```bash
npm install @base-ui/react lucide-react motion
```

**Add the shadcn `select` component (used for scroll and group primitives) if you do not already have it.**

**Copy and paste the following code into your project.**

```tsx title="halo-select.tsx"
"use client"

/**
 * Composable animated select (gradient rim, frosted fill, staggered label, trailing
 * chevron / check / spinner). API mirrors `components/ui/select.tsx`: `HaloSelect`
 * (root), `HaloSelectTrigger`, `HaloSelectValue`, `HaloSelectContent`,
 * `HaloSelectItem`, plus group/label/scroll/separator aliases.
 */
import {
  createContext,
  useCallback,
  useContext,
  useEffect,
  useId,
  useMemo,
  useRef,
  useState,
  type ComponentProps,
  type ReactNode,
} from "react"
import {
  Select as SelectPrimitive,
  type SelectRoot,
} from "@base-ui/react/select"
import { Check, ChevronDown } from "lucide-react"
import { AnimatePresence, motion, useReducedMotion } from "motion/react"

import { cn } from "@/lib/utils"
import {
  Select,
  SelectGroup,
  SelectLabel,
  SelectScrollDownButton,
  SelectScrollUpButton,
  SelectSeparator,
  SelectContent as UiSelectContent,
} from "@/components/ui/base-select"

const LABEL_STAGGER_SEC = 0.018
const LABEL_CHAR_DURATION_SEC = 0.16
const TRAILING_ACTION_DURATION_SEC = 0.2
const LIST_STAGGER_STEP_SEC = 0.055
const LIST_STAGGER_CAP = 9
const SELECTION_FLASH_MS = 560

interface BorderGlowConfig {
  opacityIdle: number
  primaryBg: string
  secondaryBg: string
  tertiaryBg: string
  primaryLeft: string[]
  secondaryLeft: string[]
  tertiaryLeft: string[]
  durationMain: number
  durationSec: number
  durationTer: number
}

export interface PolishedSelectItem {
  value: string
  label: string
  disabled?: boolean
}

type SelectValueChangeDetails = Parameters<
  NonNullable<SelectRoot.Props<PolishedSelectItem>["onValueChange"]>
>[1]

interface HaloSelectContextValue {
  translucent: boolean
  isLoading: boolean
  showSelectionFlash: boolean
  reduceMotion: boolean
  open: boolean
  setOpen: (open: boolean) => void
  isFocused: boolean
  setIsFocused: (focused: boolean) => void
  listBurst: number
  labelStagger: { key: number; text: string }
  showStaggerOverlay: boolean
  placeholder: string
  borderGlow: BorderGlowConfig
  trailingMode: "spinner" | "check" | "chevron"
  fieldId: string
}

const HaloSelectContext = createContext<HaloSelectContextValue | null>(null)

function useHaloSelect() {
  const ctx = useContext(HaloSelectContext)
  if (!ctx) {
    throw new Error(
      "Animated select components must be used within <HaloSelect>."
    )
  }
  return ctx
}

function SelectStaggeredGlyphLine({
  text,
  animateKey,
  reduceMotion,
  className,
}: {
  text: string
  animateKey: number
  reduceMotion: boolean
  className?: string
}) {
  const chars = useMemo(() => {
    const counts = new Map<string, number>()
    return Array.from(text, (char) => {
      const count = counts.get(char) ?? 0
      counts.set(char, count + 1)
      return { char, key: `${char}-${count}` }
    })
  }, [text])

  return (
    <motion.span
      animate="visible"
      className={className}
      initial="hidden"
      key={animateKey}
      variants={{
        hidden: {},
        visible: {
          transition: {
            staggerChildren: reduceMotion ? 0 : LABEL_STAGGER_SEC,
          },
        },
      }}
    >
      {chars.map((glyph) => (
        <motion.span
          className="inline-block whitespace-pre"
          key={glyph.key}
          transition={{
            duration: reduceMotion ? 0 : LABEL_CHAR_DURATION_SEC,
            ease: "easeOut",
          }}
          variants={{
            hidden: { opacity: reduceMotion ? 1 : 0 },
            visible: { opacity: 1 },
          }}
        >
          {glyph.char}
        </motion.span>
      ))}
    </motion.span>
  )
}

function TrailingSpinner({
  className,
  reduceMotion,
}: {
  className?: string
  reduceMotion: boolean
}) {
  return (
    <span
      className={cn(
        "inline-flex origin-center text-muted-foreground",
        !reduceMotion && "animate-spin",
        className
      )}
    >
      <svg
        aria-hidden="true"
        className="h-3.5 w-3.5"
        focusable="false"
        viewBox="0 0 24 24"
      >
        <circle
          className="opacity-[0.22]"
          cx="12"
          cy="12"
          fill="none"
          r="9"
          stroke="currentColor"
          strokeWidth="2.25"
        />
        <circle
          cx="12"
          cy="12"
          fill="none"
          r="9"
          stroke="currentColor"
          strokeDasharray="14 42"
          strokeLinecap="round"
          strokeWidth="2.25"
        />
      </svg>
    </span>
  )
}

const trailingMotionTransition = (reduceMotion: boolean) => ({
  duration: reduceMotion ? 0 : TRAILING_ACTION_DURATION_SEC,
  ease: "easeOut" as const,
})

const trailingCrossfade = (reduceMotion: boolean) => ({
  exit: {
    opacity: reduceMotion ? 1 : 0,
    scale: reduceMotion ? 1 : 0.25,
    filter: reduceMotion ? "none" : "blur(4px)",
  },
  initial: {
    opacity: reduceMotion ? 1 : 0,
    scale: reduceMotion ? 1 : 0.25,
    filter: reduceMotion ? "none" : "blur(4px)",
  },
})

const trailingEnter = (reduceMotion: boolean) => ({
  opacity: 1,
  scale: 1,
  filter: reduceMotion ? "none" : "blur(0px)",
})

/** Raised “thumb” surface — radius = trigger inner (~10px) − inset so corners don’t crowd the rim */
const trailingThumbSurface = cn(
  "rounded-[6px] bg-background",
  "shadow-[0_1px_0_rgba(255,255,255,0.85)_inset,0_1px_2px_rgba(0,0,0,0.07),0_2px_10px_rgba(0,0,0,0.05)]",
  "ring-1 ring-black/8 dark:ring-white/12",
  "dark:shadow-[0_1px_0_rgba(255,255,255,0.12)_inset,0_1px_3px_rgba(0,0,0,0.45),0_2px_12px_rgba(0,0,0,0.35)]",
  "fine-hover:group-hover/select:ring-2 fine-hover:group-hover/select:ring-ring/20"
)

function resolveTrailingMode(
  isLoading: boolean,
  selectionFlash: boolean,
  showSelectionFlash: boolean,
  reduceMotion: boolean
): "spinner" | "check" | "chevron" {
  if (isLoading) {
    return "spinner"
  }
  if (selectionFlash && showSelectionFlash && !reduceMotion) {
    return "check"
  }
  return "chevron"
}

/** Inset 1px inside `rounded-xl` shell → inner radius 12px − 1px = 11px (concentric with trigger). */
function SelectGradientUnderlay({
  borderGlow,
  translucent,
  reduceMotion,
}: {
  borderGlow: BorderGlowConfig
  translucent: boolean
  reduceMotion: boolean
}) {
  return (
    <div className="pointer-events-none absolute inset-0 overflow-hidden rounded-[11px]">
      {translucent ? (
        <div
          className={cn(
            "absolute inset-0",
            reduceMotion ? "opacity-[0.38]" : "opacity-50"
          )}
        >
          <div
            className="absolute inset-0 blur-2xl"
            style={{
              background:
                "radial-gradient(90% 70% at 50% 100%, rgba(255,0,128,0.28), rgba(121,40,202,0.12) 48%, transparent 62%)",
            }}
          />
          <div
            className="absolute inset-0 opacity-70 blur-xl dark:opacity-50"
            style={{
              background:
                "radial-gradient(70% 55% at 30% 0%, rgba(0,212,255,0.22), rgba(0,112,243,0.08) 45%, transparent 58%)",
            }}
          />
        </div>
      ) : null}
      <motion.div
        animate={{
          opacity: borderGlow.opacityIdle * (translucent ? 0.72 : 1),
        }}
        className="absolute inset-x-0 bottom-0 h-1/2"
        transition={{ duration: 0.25, ease: "easeOut" }}
      >
        <motion.div
          animate={{
            left: borderGlow.primaryLeft,
          }}
          className="-bottom-4 absolute h-16 w-48 blur-xl"
          style={{
            background: borderGlow.primaryBg,
          }}
          transition={{
            duration: borderGlow.durationMain,
            repeat: Number.POSITIVE_INFINITY,
            ease: "easeInOut",
          }}
        />
        <motion.div
          animate={{
            left: borderGlow.secondaryLeft,
          }}
          className="-bottom-3 absolute h-12 w-32 blur-lg"
          style={{
            background: borderGlow.secondaryBg,
          }}
          transition={{
            duration: borderGlow.durationSec,
            repeat: Number.POSITIVE_INFINITY,
            ease: "easeInOut",
          }}
        />
        <motion.div
          animate={{
            left: borderGlow.tertiaryLeft,
          }}
          className="-bottom-2 absolute h-10 w-24 blur-lg"
          style={{
            background: borderGlow.tertiaryBg,
          }}
          transition={{
            duration: borderGlow.durationTer,
            repeat: Number.POSITIVE_INFINITY,
            ease: "easeInOut",
          }}
        />
      </motion.div>
    </div>
  )
}

export type HaloSelectProps = Omit<
  ComponentProps<typeof Select>,
  "children" | "onValueChange"
> & {
  children: ReactNode
  /** Frosted glass style on the trigger track */
  translucent?: boolean
  isLoading?: boolean
  showSelectionFlash?: boolean
  /** Shown when no value is selected; also forwarded to `HaloSelectValue` via context */
  placeholder?: string
  className?: string
  onValueChange?: (
    value: unknown,
    eventDetails: SelectValueChangeDetails
  ) => void
}

function staggerDurationMs(text: string, reduceMotion: boolean): number {
  if (reduceMotion || text.length === 0) {
    return 0
  }
  const n = text.length
  const runSec = (n - 1) * LABEL_STAGGER_SEC + LABEL_CHAR_DURATION_SEC
  return runSec * 1000 + 80
}

/**
 * Root: Base UI `Select` plus internal context for stagger, rim, and trailing indicator.
 * Compose with `HaloSelectTrigger`, `HaloSelectContent`, and items — same idea as
 * [`Select`](components/ui/select.tsx) + `SelectTrigger` + `SelectContent`.
 */
export function HaloSelect({
  children,
  translucent = false,
  isLoading = false,
  showSelectionFlash = true,
  placeholder = "Select an option",
  className,
  value: valueProp,
  defaultValue,
  onValueChange,
  onOpenChange: onOpenChangeUser,
  ...selectProps
}: HaloSelectProps) {
  const reduceMotion = useReducedMotion() ?? false
  const generatedId = useId()
  const fieldId = selectProps.id ?? generatedId
  const [open, setOpen] = useState(false)
  const [isFocused, setIsFocused] = useState(false)
  const [listBurst, setListBurst] = useState(0)
  const [labelStagger, setLabelStagger] = useState({ key: 0, text: "" })
  const [isStaggering, setIsStaggering] = useState(false)
  const [selectionFlash, setSelectionFlash] = useState(false)
  const flashTimerRef = useRef<number | null>(null)

  const isControlled = valueProp !== undefined
  const isActive = isFocused || open
  const showBlobTranslucent = translucent

  const borderGlow = useMemo((): BorderGlowConfig => {
    let idleOpacity: number
    if (showBlobTranslucent) {
      idleOpacity = isActive ? 0.92 : 0.76
    } else {
      idleOpacity = isActive ? 0.82 : 0.58
    }
    return {
      opacityIdle: idleOpacity,
      primaryBg: "linear-gradient(90deg, #ff0080, #7928ca, #00d4ff, #0070f3)",
      secondaryBg: "linear-gradient(90deg, #ff4d4d, #f9cb28, #ff0080)",
      tertiaryBg: "linear-gradient(90deg, #0070f3, #00d4ff, #7928ca)",
      primaryLeft: ["-5%", "75%", "-5%"],
      secondaryLeft: ["65%", "10%", "65%"],
      tertiaryLeft: ["25%", "55%", "25%"],
      durationMain: 6,
      durationSec: 5,
      durationTer: 4,
    }
  }, [showBlobTranslucent, isActive])

  useEffect(() => {
    if (open) {
      setListBurst((b) => b + 1)
    }
  }, [open])

  useEffect(() => {
    return () => {
      if (flashTimerRef.current) {
        window.clearTimeout(flashTimerRef.current)
      }
    }
  }, [])

  useEffect(() => {
    const text = labelStagger.text
    if (reduceMotion || !text || open) {
      setIsStaggering(false)
      return
    }
    setIsStaggering(true)
    const ms = staggerDurationMs(text, reduceMotion)
    const t = window.setTimeout(() => setIsStaggering(false), ms)
    return () => window.clearTimeout(t)
  }, [labelStagger, open, reduceMotion])

  const handleValueChange = useCallback(
    (next: unknown, eventDetails: SelectValueChangeDetails) => {
      onValueChange?.(next, eventDetails)
      if (
        next &&
        typeof next === "object" &&
        "label" in next &&
        typeof (next as PolishedSelectItem).label === "string"
      ) {
        setLabelStagger((s) => ({
          key: s.key + 1,
          text: (next as PolishedSelectItem).label,
        }))
        if (showSelectionFlash && !reduceMotion) {
          if (flashTimerRef.current) {
            window.clearTimeout(flashTimerRef.current)
          }
          setSelectionFlash(true)
          flashTimerRef.current = window.setTimeout(() => {
            setSelectionFlash(false)
            flashTimerRef.current = null
          }, SELECTION_FLASH_MS)
        }
      } else {
        setLabelStagger({ key: 0, text: "" })
      }
    },
    [onValueChange, reduceMotion, showSelectionFlash]
  )

  const showStaggerOverlay =
    Boolean(labelStagger.text) && isStaggering && !open && !reduceMotion

  const trailingMode = resolveTrailingMode(
    isLoading,
    selectionFlash,
    showSelectionFlash,
    reduceMotion
  )

  const contextValue = useMemo(
    (): HaloSelectContextValue => ({
      translucent: showBlobTranslucent,
      isLoading,
      showSelectionFlash,
      reduceMotion,
      open,
      setOpen,
      isFocused,
      setIsFocused,
      listBurst,
      labelStagger,
      showStaggerOverlay,
      placeholder,
      borderGlow,
      trailingMode,
      fieldId,
    }),
    [
      showBlobTranslucent,
      isLoading,
      showSelectionFlash,
      reduceMotion,
      open,
      isFocused,
      listBurst,
      labelStagger,
      showStaggerOverlay,
      placeholder,
      borderGlow,
      trailingMode,
      fieldId,
    ]
  )

  return (
    <HaloSelectContext.Provider value={contextValue}>
      <div
        className={cn(
          "relative inline-block w-max min-w-0 max-w-full",
          className
        )}
      >
        <Select
          {...selectProps}
          defaultValue={
            isControlled
              ? undefined
              : (defaultValue as PolishedSelectItem | undefined)
          }
          disabled={selectProps.disabled || isLoading}
          onOpenChange={(nextOpen, eventDetails) => {
            setOpen(nextOpen)
            onOpenChangeUser?.(nextOpen, eventDetails)
          }}
          onValueChange={handleValueChange}
          open={open}
          value={isControlled ? (valueProp ?? null) : undefined}
        >
          {children}
        </Select>
      </div>
    </HaloSelectContext.Provider>
  )
}

export type HaloSelectTriggerProps = SelectPrimitive.Trigger.Props

/**
 * Gradient rim + pill trigger shell. Place `HaloSelectValue` and
 * `HaloSelectTrailingIndicator` inside (same role as `SelectTrigger` + value + icon).
 */
export function HaloSelectTrigger({
  className,
  children,
  onBlur,
  onFocus,
  ...props
}: HaloSelectTriggerProps) {
  const {
    translucent,
    isLoading,
    open,
    reduceMotion,
    isFocused,
    setIsFocused,
    borderGlow,
    fieldId,
  } = useHaloSelect()

  const ringActive = isFocused || open

  return (
    <div className="relative w-fit max-w-full rounded-xl p-px">
      <SelectGradientUnderlay
        borderGlow={borderGlow}
        reduceMotion={reduceMotion}
        translucent={translucent}
      />
      <SelectPrimitive.Trigger
        aria-busy={isLoading || props["aria-busy"]}
        className={cn(
          "relative inline-flex min-h-11 min-w-34 max-w-[min(100%,20rem)] items-center gap-2 rounded-[11px] py-2 pr-2 pl-3 text-left",
          "whitespace-nowrap antialiased",
          "border border-[color-mix(in_oklch,var(--border)_72%,oklch(0.88_0.06_300))] bg-transparent",
          /* Shell stack: top inset highlight + contact shadow + soft lift — aligned with HaloSwitch / HaloBadge */
          "shadow-[0_1px_0_rgba(255,255,255,0.55)_inset,0_1px_2px_rgba(0,0,0,0.03),0_6px_18px_rgba(0,0,0,0.04)]",
          "dark:border-border/80 dark:shadow-[0_1px_0_rgba(255,255,255,0.05)_inset,0_1px_2px_rgba(0,0,0,0.28),0_6px_22px_rgba(0,0,0,0.32)]",
          "group/select outline-none transition-[background-color,backdrop-filter,border-color,box-shadow] duration-200 ease-out",
          "fine-hover:hover:border-ring/50 fine-hover:hover:shadow-[0_1px_0_rgba(255,255,255,0.6)_inset,0_1px_3px_rgba(0,0,0,0.05),0_8px_22px_rgba(0,0,0,0.06)]",
          "dark:fine-hover:hover:border-zinc-500/60 dark:fine-hover:hover:shadow-[0_1px_0_rgba(255,255,255,0.08)_inset,0_2px_4px_rgba(0,0,0,0.38),0_10px_28px_rgba(0,0,0,0.42)]",
          "focus-visible:border-ring/80 focus-visible:ring-2 focus-visible:ring-ring/30",
          "disabled:cursor-not-allowed disabled:opacity-50",
          "data-placeholder:text-muted-foreground",
          ringActive &&
            "border-ring/70 shadow-[0_1px_0_rgba(255,255,255,0.14)_inset,0_1px_2px_rgba(0,0,0,0.09),0_4px_14px_rgba(0,0,0,0.11)] dark:border-zinc-500/55 dark:shadow-[0_1px_0_rgba(255,255,255,0.08)_inset,0_1px_2px_rgba(0,0,0,0.42),0_4px_20px_rgba(0,0,0,0.48)]",
          translucent && "backdrop-blur-xl backdrop-saturate-150",
          className
        )}
        data-slot="halo-select-trigger"
        disabled={props.disabled ?? isLoading}
        id={props.id ?? fieldId}
        onBlur={(e) => {
          setIsFocused(false)
          onBlur?.(e)
        }}
        onFocus={(e) => {
          setIsFocused(true)
          onFocus?.(e)
        }}
        style={{
          background: translucent
            ? "linear-gradient(to bottom, color-mix(in oklch, var(--card) 82%, oklch(0.92 0.04 330)) 0%, color-mix(in oklch, var(--card) 78%, oklch(0.88 0.06 285)) 50%, color-mix(in oklch, var(--card) 76%, oklch(0.9 0.05 245)) 100%)"
            : "linear-gradient(to bottom, color-mix(in oklch, var(--card) 95%, oklch(0.97 0.02 315)) 0%, color-mix(in oklch, var(--card) 93%, oklch(0.96 0.025 280)) 100%)",
          ...props.style,
        }}
        {...props}
        type="button"
      >
        {children}
      </SelectPrimitive.Trigger>
    </div>
  )
}

export function HaloSelectValue({
  className,
  placeholder: placeholderProp,
  ...props
}: SelectPrimitive.Value.Props) {
  const {
    placeholder: contextPlaceholder,
    showStaggerOverlay,
    labelStagger,
    reduceMotion,
  } = useHaloSelect()

  return (
    <div className="relative min-w-0 flex-1 text-sm/relaxed">
      <SelectPrimitive.Value
        className={cn(
          "block min-w-0 flex-1 truncate bg-transparent text-foreground text-sm/relaxed",
          showStaggerOverlay && "text-transparent",
          className
        )}
        data-slot="halo-select-value"
        placeholder={placeholderProp ?? contextPlaceholder}
        {...props}
      />
      {showStaggerOverlay && (
        <SelectStaggeredGlyphLine
          animateKey={labelStagger.key}
          className="pointer-events-none absolute inset-y-0 left-0 flex items-center text-foreground"
          reduceMotion={reduceMotion}
          text={labelStagger.text}
        />
      )}
    </div>
  )
}

/** Chevron / check / spinner aligned with the trigger (like `SelectPrimitive.Icon` usage). */
export function HaloSelectTrailingIndicator({
  className,
}: {
  className?: string
}) {
  const { trailingMode, reduceMotion, open } = useHaloSelect()
  const cross = trailingCrossfade(reduceMotion)
  const transition = trailingMotionTransition(reduceMotion)

  return (
    <div className={cn("relative size-8 shrink-0", className)}>
      {/*
        Thumb shell stays static; only the glyph crossfades (enter/exit). `initial` must
        not be false on animated children so the first paint is not stuck invisible.
      */}
      <div
        aria-hidden
        className={cn(
          "pointer-events-none absolute inset-0 flex items-center justify-center",
          trailingThumbSurface
        )}
      />
      <div className="absolute inset-0 flex items-center justify-center">
        <AnimatePresence mode="sync">
          {trailingMode === "spinner" && (
            <motion.span
              animate={trailingEnter(reduceMotion)}
              aria-hidden
              className="absolute inset-0 flex items-center justify-center text-muted-foreground"
              exit={cross.exit}
              initial={cross.initial}
              key="trail-spin"
              transition={transition}
            >
              <TrailingSpinner reduceMotion={reduceMotion} />
            </motion.span>
          )}
          {trailingMode === "check" && (
            <motion.span
              animate={trailingEnter(reduceMotion)}
              aria-hidden
              className="absolute inset-0 flex items-center justify-center text-emerald-600 dark:text-emerald-400"
              exit={cross.exit}
              initial={cross.initial}
              key="trail-check"
              transition={transition}
            >
              <Check className="h-3.5 w-3.5" strokeWidth={2.5} />
            </motion.span>
          )}
          {trailingMode === "chevron" && (
            <motion.span
              animate={trailingEnter(reduceMotion)}
              aria-hidden
              className="absolute inset-0 flex items-center justify-center text-muted-foreground"
              exit={cross.exit}
              initial={cross.initial}
              key="trail-chevron"
              transition={transition}
            >
              <motion.span
                animate={{
                  rotate: !reduceMotion && open ? 180 : 0,
                }}
                className="inline-flex"
                transition={transition}
              >
                <ChevronDown className="h-3.5 w-3.5" />
              </motion.span>
            </motion.span>
          )}
        </AnimatePresence>
      </div>
    </div>
  )
}

export function HaloSelectContent({
  className,
  side = "bottom",
  sideOffset = 6,
  align = "start",
  ...props
}: ComponentProps<typeof UiSelectContent>) {
  return (
    <UiSelectContent
      {...props}
      align={align}
      alignItemWithTrigger={true}
      className={cn(
        "max-h-72 w-(--anchor-width) min-w-(--anchor-width) rounded-xl border border-[color-mix(in_oklch,var(--border)_72%,oklch(0.88_0.06_300))] bg-popover/95",
        "shadow-[0_1px_0_rgba(255,255,255,0.1)_inset,0_1px_2px_rgba(0,0,0,0.04),0_4px_14px_rgba(0,0,0,0.05)]",
        "dark:border-border/80 dark:shadow-[0_1px_0_rgba(255,255,255,0.06)_inset,0_1px_2px_rgba(0,0,0,0.35),0_4px_20px_rgba(0,0,0,0.35)]",
        "ring-1 ring-black/8 backdrop-blur-xl backdrop-saturate-150 dark:ring-white/12",
        "data-closed:fade-out-0 data-open:fade-in-0 data-closed:animate-out data-open:animate-in",
        "data-closed:zoom-out-95 data-open:zoom-in-95",
        "data-[side=bottom]:slide-in-from-top-2",
        "duration-150 ease-out",
        className
      )}
      side={side}
      sideOffset={sideOffset}
    />
  )
}

export function HaloSelectItem({
  className,
  children,
  index = 0,
  label,
  value,
  ...props
}: SelectPrimitive.Item.Props & { index?: number }) {
  const { listBurst, reduceMotion } = useHaloSelect()
  const capped = Math.min(index, LIST_STAGGER_CAP)
  const delay = reduceMotion ? 0 : capped * LIST_STAGGER_STEP_SEC
  const valueKey =
    value !== undefined &&
    value !== null &&
    typeof value === "object" &&
    "value" in (value as PolishedSelectItem)
      ? (value as PolishedSelectItem).value
      : String(value)

  return (
    <SelectPrimitive.Item
      className={cn(
        "relative flex min-h-11 w-full cursor-default select-none items-center gap-2 rounded-lg px-2.5 py-2.5",
        "text-foreground text-sm antialiased outline-none",
        "transition-[background-color,color,box-shadow] duration-150 ease-out",
        "data-disabled:pointer-events-none data-disabled:opacity-50",
        "data-highlighted:bg-accent data-highlighted:text-accent-foreground",
        "data-highlighted:shadow-[inset_0_1px_0_rgba(255,255,255,0.06)] dark:data-highlighted:shadow-[inset_0_1px_0_rgba(255,255,255,0.04)]",
        className
      )}
      data-slot="halo-select-item"
      label={label}
      value={value}
      {...props}
    >
      <motion.div
        animate={{ filter: "blur(0px)", opacity: 1, y: 0 }}
        className="flex w-full min-w-0 items-center gap-2"
        initial={
          reduceMotion ? false : { filter: "blur(4px)", opacity: 0, y: 4 }
        }
        key={`${listBurst}-${valueKey}`}
        transition={{
          delay,
          duration: 0.22,
          ease: [0.2, 0, 0, 1],
        }}
      >
        <SelectPrimitive.ItemText className="min-w-0 flex-1 truncate text-pretty">
          {children}
        </SelectPrimitive.ItemText>
        <SelectPrimitive.ItemIndicator className="flex shrink-0 text-foreground">
          <Check
            aria-hidden
            className="h-3.5 w-3.5 opacity-90"
            strokeWidth={2.5}
          />
        </SelectPrimitive.ItemIndicator>
      </motion.div>
    </SelectPrimitive.Item>
  )
}

export const HaloSelectGroup = SelectGroup
export const HaloSelectLabel = SelectLabel
export const HaloSelectScrollUpButton = SelectScrollUpButton
export const HaloSelectScrollDownButton = SelectScrollDownButton
export const HaloSelectSeparator = SelectSeparator
```

**Update the import paths to match your project setup.**
