Iconiq
uib-autocompletesource: ok · refreshed
Autocomplete (Base UI)
Base UI autocomplete with list filtering, suggestion highlight motion, and the same Iconiq input shell as the combobox install.
npx shadcn add "https://iconiqui.com/r/b-autocomplete.json"free
Interaction
dropdown
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · mediumui-primitives@base-ui/react
animationmotion
iconslucide-react
Files1 · source cached 10/2/2026, 4:14:43 PM
b-autocomplete.tsx
"use client";
import { Autocomplete as AutocompletePrimitive } from "@base-ui/react/autocomplete";
import { Input as InputPrimitive } from "@base-ui/react/input";
import { ScrollArea as ScrollAreaPrimitive } from "@base-ui/react/scroll-area";
import { ChevronsUpDown, X } from "lucide-react";
import { motion, useReducedMotion } from "motion/react";
import * as React from "react";
import { cn } from "@/lib/utils";
const controlCornerClassName =
"rounded-lg supports-[corner-shape:squircle]:corner-squircle supports-[corner-shape:squircle]:rounded-[11px]";
const controlCornerInheritClassName =
"rounded-[inherit] supports-[corner-shape:squircle]:[corner-shape:inherit]";
const componentThemeClassName =
"[--ic-background:#ffffff] [--ic-foreground:#111111] [--ic-primary:#111111] [--ic-secondary:#646b75] [--ic-surface-border:#e9edf2] [--ic-border:#e3e7ec] [--ic-card:#ffffff] [--ic-card-foreground:#111111] [--ic-muted:#f5f7fa] [--ic-muted-foreground:#6d7480] [--ic-accent:#f3f5f8] [--color-accent:var(--ic-accent)] [--color-accent-foreground:var(--ic-accent-foreground)] [--ic-accent-foreground:#111111] [--ic-input:#e3e7ec] [--ic-ring:rgba(17,17,17,0.16)] [--ic-destructive:#dc2626] [--ic-paper:#fcfcfd] [--ic-popover-foreground:#111111] [--ic-brand:#0ea5e9] [--ic-brand-soft:#bae6fd] [--ic-shadow-soft:0_18px_38px_-24px_rgba(15,23,42,0.35)] [--ic-chart-1:oklch(0.52_0.19_254)] [--ic-chart-2:oklch(0.74_0.11_232)] [--ic-chart-3:oklch(0.42_0.16_262)] [--ic-chart-4:oklch(0.84_0.07_228)] [--ic-chart-5:oklch(0.62_0.14_240)] [--color-background:var(--ic-background)] [--color-foreground:var(--ic-foreground)] [--color-primary:var(--ic-primary)] [--color-secondary:var(--ic-secondary)] [--color-border:var(--ic-border)] [--color-card:var(--ic-card)] [--color-card-foreground:var(--ic-card-foreground)] [--color-muted:var(--ic-muted)] [--color-muted-foreground:var(--ic-muted-foreground)] [--color-accent:var(--ic-accent)] [--color-accent-foreground:var(--ic-accent-foreground)] [--color-input:var(--ic-input)] [--color-ring:var(--ic-ring)] [--color-destructive:var(--ic-destructive)] [--color-paper:var(--ic-paper)] [--color-popover-foreground:var(--ic-popover-foreground)] [--color-brand:var(--ic-brand)] [--color-brand-soft:var(--ic-brand-soft)] [--color-chart-1:var(--ic-chart-1)] [--color-chart-2:var(--ic-chart-2)] [--color-chart-3:var(--ic-chart-3)] [--color-chart-4:var(--ic-chart-4)] [--color-chart-5:var(--ic-chart-5)] dark:[--ic-background:#111111] dark:[--ic-foreground:#f6f3ec] dark:[--ic-primary:#f6f3ec] dark:[--ic-secondary:#cbc6bb] dark:[--ic-surface-border:#2a2a25] dark:[--ic-border:#2b2a25] dark:[--ic-card:#111111] dark:[--ic-card-foreground:#f6f3ec] dark:[--ic-muted:#171716] dark:[--ic-muted-foreground:#9a958a] dark:[--ic-accent:#1a1a18] [--color-accent:var(--ic-accent)] [--color-accent-foreground:var(--ic-accent-foreground)] dark:[--ic-accent-foreground:#f6f3ec] dark:[--ic-input:#2b2a25] dark:[--ic-ring:rgba(246,243,236,0.18)] dark:[--ic-destructive:#f87171] dark:[--ic-paper:#171716] dark:[--ic-popover-foreground:#f6f3ec] dark:[--ic-brand:#38bdf8] dark:[--ic-brand-soft:#0c4a6e] dark:[--ic-shadow-soft:0_20px_44px_-28px_rgba(0,0,0,0.6)] dark:[--ic-chart-1:oklch(0.68_0.17_250)] dark:[--ic-chart-2:oklch(0.82_0.09_225)] dark:[--ic-chart-3:oklch(0.58_0.15_260)] dark:[--ic-chart-4:oklch(0.75_0.12_235)] dark:[--ic-chart-5:oklch(0.88_0.06_220)]";
type AutocompleteRootProps<Value> = Omit<
AutocompletePrimitive.Root.Props<Value>,
"items"
> & {
items: readonly Value[];
};
const INSTANT_TRANSITION = { duration: 0 } as const;
type AutocompleteContextValue = {
actionsRef: React.RefObject<AutocompletePrimitive.Root.Actions | null>;
activeHighlightId: string;
activeValue: unknown;
open: boolean;
prefersReducedMotion: boolean;
setActiveValue: React.Dispatch<React.SetStateAction<unknown>>;
setOpen: (open: boolean) => void;
skipExitAnimationRef: React.MutableRefObject<boolean>;
};
type DivRenderProps = React.HTMLAttributes<HTMLDivElement> & {
children?: React.ReactNode;
className?: string;
ref?: React.Ref<HTMLDivElement>;
style?: React.CSSProperties;
};
type InputRenderProps = React.InputHTMLAttributes<HTMLInputElement> & {
className?: string;
ref?: React.Ref<HTMLInputElement>;
style?: React.CSSProperties;
};
const AutocompleteContext =
React.createContext<AutocompleteContextValue | null>(null);
function useAutocompleteContext(componentName: string) {
const context = React.useContext(AutocompleteContext);
if (!context) {
throw new Error(`${componentName} must be used inside Autocomplete`);
}
return context;
}
function setRef<T>(ref: React.Ref<T> | undefined, value: T | null) {
if (typeof ref === "function") {
ref(value);
return;
}
if (ref) {
(ref as React.MutableRefObject<T | null>).current = value;
}
}
function composeEventHandlers<Event extends React.SyntheticEvent>(
originalEventHandler: ((event: Event) => void) | undefined,
eventHandler: (event: Event) => void
) {
return (event: Event) => {
originalEventHandler?.(event);
if (!event.defaultPrevented) {
eventHandler(event);
}
};
}
function resolveStateClassName<State>(
className: string | ((state: State) => string | undefined) | undefined,
state: State
) {
return typeof className === "function" ? className(state) : className;
}
function resolvePopupProps(popupProps: DivRenderProps) {
const {
children: _popupChildren,
className: popupClassName,
onAnimationEnd: _onAnimationEnd,
onAnimationIteration: _onAnimationIteration,
onAnimationStart: _onAnimationStart,
onDrag: _onDrag,
onDragEnd: _onDragEnd,
onDragEnter: _onDragEnter,
onDragExit: _onDragExit,
onDragLeave: _onDragLeave,
onDragOver: _onDragOver,
onDragStart: _onDragStart,
onDrop: _onDrop,
ref: popupRef,
style: popupStyle,
...resolvedPopupProps
} = popupProps;
return {
popupClassName,
popupRef,
popupStyle,
resolvedPopupProps,
};
}
function resolveItemProps(itemProps: DivRenderProps) {
const {
children: _itemChildren,
className: itemClassName,
onAnimationEnd: _onAnimationEnd,
onAnimationIteration: _onAnimationIteration,
onAnimationStart: _onAnimationStart,
onDrag: _onDrag,
onDragEnd: _onDragEnd,
onDragEnter: _onDragEnter,
onDragExit: _onDragExit,
onDragLeave: _onDragLeave,
onDragOver: _onDragOver,
onDragStart: _onDragStart,
onDrop: _onDrop,
ref: itemRef,
style: itemStyle,
...resolvedItemProps
} = itemProps;
return {
itemClassName,
itemRef,
itemStyle,
resolvedItemProps,
};
}
const FLUID_EASE = [0.16, 1, 0.3, 1] as const;
const EXIT_EASE = [0.4, 0, 0.6, 1] as const;
const POPUP_SPRING = {
type: "spring" as const,
stiffness: 260,
damping: 32,
mass: 0.95,
};
const HIGHLIGHT_SPRING = {
type: "spring" as const,
stiffness: 460,
damping: 40,
mass: 0.82,
};
const popupMotion = {
animate: { opacity: 1, scale: 1, y: 0 },
closed: { opacity: 0, scale: 0.985, y: -5 },
initial: { opacity: 0, scale: 0.985, y: -5 },
openTransition: {
opacity: { duration: 0.34, ease: FLUID_EASE },
scale: POPUP_SPRING,
y: POPUP_SPRING,
},
closedTransition: {
opacity: { duration: 0.22, ease: EXIT_EASE },
scale: { duration: 0.22, ease: EXIT_EASE },
y: { duration: 0.22, ease: EXIT_EASE },
},
};
const autocompleteListScrollbarClassName =
"z-10 my-1.5 mr-0.5 w-1 shrink-0 touch-none select-none opacity-0 transition-opacity duration-150 before:absolute before:left-1/2 before:h-full before:w-5 before:-translate-x-1/2 before:content-[''] data-hovering:pointer-events-auto data-hovering:opacity-100 data-scrolling:pointer-events-auto data-scrolling:opacity-100 data-scrolling:duration-0";
const autocompleteListThumbClassName =
"relative rounded-full bg-muted-foreground/50 bg-[color:color-mix(in_oklch,var(--ic-muted-foreground),transparent_35%)]";
const autocompleteInputShellClassName =
"group flex h-11 w-full items-center gap-1 border border-input bg-background pl-3 pr-2 text-base transition-[border-color,box-shadow] sm:text-sm hover:border-ring/40 focus-within:border-ring focus-within:ring-1 focus-within:ring-ring/25 [&:has(input[aria-invalid=true])]:border-destructive [&:has(input[aria-invalid=true])]:ring-1 [&:has(input[aria-invalid=true])]:ring-destructive/20 dark:[&:has(input[aria-invalid=true])]:border-destructive/50 dark:[&:has(input[aria-invalid=true])]:ring-destructive/40";
const autocompleteInputClassName =
"h-full w-full min-w-0 flex-1 bg-transparent text-[16px] text-foreground placeholder:text-muted-foreground focus:outline-none disabled:cursor-not-allowed sm:text-sm";
const autocompletePopupPanelClassName =
"w-[var(--anchor-width)] max-w-[var(--available-width)] transform-gpu overflow-hidden border border-neutral-200/60 bg-white text-neutral-900 shadow-none dark:border-neutral-700/60 dark:bg-neutral-950 dark:text-neutral-100";
const MAX_MENU_HEIGHT = 256;
const VIEWPORT_PADDING = 12;
const autocompleteCollisionAvoidance = {
align: "shift" as const,
fallbackAxisSide: "none" as const,
side: "none" as const,
};
const chevronTransition = {
type: "spring" as const,
stiffness: 380,
damping: 30,
mass: 0.8,
};
function Autocomplete<Value>({
actionsRef: actionsRefProp,
autoHighlight = true,
children,
defaultOpen = false,
highlightItemOnHover = true,
items,
modal = false,
onItemHighlighted,
onOpenChange,
open: openProp,
openOnInputClick = false,
...props
}: AutocompleteRootProps<Value>) {
const internalActionsRef =
React.useRef<AutocompletePrimitive.Root.Actions | null>(null);
const actionsRef = actionsRefProp ?? internalActionsRef;
const isOpenControlled = openProp !== undefined;
const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen);
const [activeValue, setActiveValue] = React.useState<unknown>();
const open = isOpenControlled ? openProp : uncontrolledOpen;
const activeHighlightId = React.useId();
const skipExitAnimationRef = React.useRef(false);
const prefersReducedMotion = useReducedMotion() === true;
const setOpen = React.useCallback(
(nextOpen: boolean) => {
if (!isOpenControlled) {
setUncontrolledOpen(nextOpen);
}
if (!nextOpen) {
setActiveValue(undefined);
}
},
[isOpenControlled]
);
const handleItemHighlighted = React.useCallback<
NonNullable<AutocompletePrimitive.Root.Props<Value>["onItemHighlighted"]>
>(
(highlightedValue, eventDetails) => {
setActiveValue(highlightedValue);
onItemHighlighted?.(highlightedValue, eventDetails);
},
[onItemHighlighted]
);
const handleOpenChange = React.useCallback<
NonNullable<AutocompletePrimitive.Root.Props<Value>["onOpenChange"]>
>(
(nextOpen, eventDetails) => {
if (!nextOpen && eventDetails.reason === "item-press") {
skipExitAnimationRef.current = true;
} else if (nextOpen) {
skipExitAnimationRef.current = false;
}
if (!eventDetails.isCanceled) {
setOpen(nextOpen);
}
onOpenChange?.(nextOpen, eventDetails);
},
[onOpenChange, setOpen]
);
return (
<AutocompleteContext.Provider
value={{
actionsRef,
activeHighlightId,
activeValue,
open,
prefersReducedMotion,
setActiveValue,
setOpen,
skipExitAnimationRef,
}}
>
<AutocompletePrimitive.Root
{...props}
actionsRef={actionsRef}
autoHighlight={autoHighlight}
highlightItemOnHover={highlightItemOnHover}
items={items}
modal={modal}
onItemHighlighted={handleItemHighlighted}
onOpenChange={handleOpenChange}
open={open}
openOnInputClick={openOnInputClick}
>
{children}
</AutocompletePrimitive.Root>
</AutocompleteContext.Provider>
);
}
function AutocompleteValue({ ...props }: AutocompletePrimitive.Value.Props) {
return (
<AutocompletePrimitive.Value data-slot="autocomplete-value" {...props} />
);
}
function AutocompleteIcon({
className,
...props
}: AutocompletePrimitive.Icon.Props) {
return (
<AutocompletePrimitive.Icon
className={cn(
"pointer-events-none flex shrink-0 items-center text-muted-foreground",
className
)}
data-slot="autocomplete-icon"
{...props}
/>
);
}
function AutocompleteClear({
className,
disabled: disabledProp,
...props
}: AutocompletePrimitive.Clear.Props) {
return (
<AutocompletePrimitive.Clear
disabled={disabledProp}
{...props}
render={(clearProps) => {
const {
className: _clearClassName,
ref: clearRef,
style: _clearStyle,
...resolvedClearProps
} = clearProps as React.ComponentProps<"button"> & {
ref?: React.Ref<HTMLButtonElement>;
};
return (
<button
{...resolvedClearProps}
aria-label="Clear input"
className={cn(
"flex size-8 shrink-0 items-center justify-center overflow-hidden rounded-md text-muted-foreground/70 transition-colors hover:bg-accent/60 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/30 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
data-slot="autocomplete-clear"
ref={clearRef}
type="button"
>
<X className="h-3.5 w-3.5" />
</button>
);
}}
/>
);
}
function AutocompleteTrigger({
className,
children,
disabled = false,
...props
}: AutocompletePrimitive.Trigger.Props) {
const { open, prefersReducedMotion } = useAutocompleteContext(
"AutocompleteTrigger"
);
return (
<AutocompletePrimitive.Trigger
aria-expanded={open}
aria-label={open ? "Collapse suggestions" : "Open suggestions"}
className={cn(
"flex size-8 shrink-0 items-center justify-center text-muted-foreground transition-colors hover:bg-accent/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/30 disabled:cursor-not-allowed disabled:opacity-50",
controlCornerClassName,
className
)}
data-slot="autocomplete-trigger"
disabled={disabled}
type="button"
{...props}
>
{children}
<motion.span
animate={{ rotate: open ? 180 : 0 }}
transition={
prefersReducedMotion ? INSTANT_TRANSITION : chevronTransition
}
>
<ChevronsUpDown className="h-4 w-4" />
</motion.span>
</AutocompletePrimitive.Trigger>
);
}
function AutocompleteInput({
className,
children,
disabled = false,
id: idProp,
label,
labelClassName,
ref: refProp,
render: userRender,
showClear = true,
showTrigger = false,
...props
}: AutocompletePrimitive.Input.Props & {
label?: React.ReactNode;
labelClassName?: string;
showClear?: boolean;
showTrigger?: boolean;
}) {
const { open, setOpen } = useAutocompleteContext("AutocompleteInput");
const inputRef = React.useRef<HTMLInputElement | null>(null);
const generatedId = React.useId();
const inputId = idProp ?? generatedId;
const inputGroup = (
<AutocompletePrimitive.InputGroup
className={cn(
componentThemeClassName,
controlCornerClassName,
autocompleteInputShellClassName,
disabled && "cursor-not-allowed opacity-50",
open && "border-ring ring-1 ring-ring/25",
label ? undefined : className
)}
data-slot="autocomplete-input"
onClick={(event) => {
if (disabled) return;
const target = event.target as HTMLElement;
if (
target.closest(
'[data-slot="autocomplete-clear"], [data-slot="autocomplete-trigger"]'
)
) {
return;
}
inputRef.current?.focus();
}}
>
<AutocompletePrimitive.Input
{...props}
disabled={disabled}
id={inputId}
onKeyDown={composeEventHandlers(props.onKeyDown, (event) => {
if (event.key === "Tab") {
setOpen(false);
}
})}
render={(inputProps, inputState) => {
if (userRender) {
return typeof userRender === "function"
? userRender(inputProps, inputState)
: userRender;
}
const {
className: primitiveClassName,
ref: primitiveRef,
style: primitiveStyle,
...resolvedInputProps
} = inputProps as InputRenderProps;
return (
<InputPrimitive
{...resolvedInputProps}
disabled={disabled}
id={inputId}
render={(baseInputProps) => {
const {
className: baseClassName,
ref: baseRef,
style: baseStyle,
...baseResolvedProps
} = baseInputProps as InputRenderProps;
return (
<input
{...baseResolvedProps}
autoComplete="off"
className={cn(
autocompleteInputClassName,
primitiveClassName,
baseClassName
)}
data-slot="autocomplete-input-control"
ref={(node) => {
inputRef.current = node;
setRef(primitiveRef, node);
setRef(baseRef, node);
setRef(refProp, node);
}}
style={{ ...primitiveStyle, ...baseStyle }}
/>
);
}}
/>
);
}}
/>
{(showClear || showTrigger) && (
<div className="ml-1.5 flex shrink-0 items-center gap-0.5">
{showClear ? <AutocompleteClear disabled={disabled} /> : null}
{showTrigger ? <AutocompleteTrigger disabled={disabled} /> : null}
</div>
)}
{children}
</AutocompletePrimitive.InputGroup>
);
if (!label) {
return inputGroup;
}
return (
<div className={cn("flex w-full flex-col gap-2", className)}>
<label
className={cn("font-medium text-foreground text-sm", labelClassName)}
htmlFor={inputId}
>
{label}
</label>
{inputGroup}
</div>
);
}
function AutocompleteContentPanel({
children,
className,
popupClassName,
popupRef,
popupState,
popupStyle,
resolvedPopupProps,
}: {
children: React.ReactNode;
className?:
| string
| ((state: AutocompletePrimitive.Popup.State) => string | undefined);
popupClassName?: string;
popupRef?: React.Ref<HTMLDivElement>;
popupState: AutocompletePrimitive.Popup.State;
popupStyle?: React.CSSProperties;
resolvedPopupProps: Omit<
DivRenderProps,
"children" | "className" | "ref" | "style"
>;
}) {
const { actionsRef, prefersReducedMotion, skipExitAnimationRef } =
useAutocompleteContext("AutocompleteContentPanel");
const isPopupVisible = popupState.open;
const skipExitAnimation = !popupState.open && skipExitAnimationRef.current;
return (
<div
{...resolvedPopupProps}
ref={(node) => {
setRef(popupRef, node);
}}
role="presentation"
style={{
...popupStyle,
transformOrigin: "var(--transform-origin)",
}}
>
<motion.div
animate={isPopupVisible ? popupMotion.animate : popupMotion.closed}
className={cn(
componentThemeClassName,
controlCornerClassName,
autocompletePopupPanelClassName,
popupClassName,
resolveStateClassName(className, popupState)
)}
initial={
popupState.transitionStatus === "starting"
? popupMotion.initial
: false
}
onAnimationComplete={() => {
if (!popupState.open) {
skipExitAnimationRef.current = false;
actionsRef.current?.unmount();
}
}}
style={{
pointerEvents: isPopupVisible ? undefined : "none",
transformOrigin: "var(--transform-origin)",
}}
transition={
prefersReducedMotion
? INSTANT_TRANSITION
: isPopupVisible
? popupMotion.openTransition
: skipExitAnimation
? INSTANT_TRANSITION
: popupMotion.closedTransition
}
>
{children}
</motion.div>
</div>
);
}
function AutocompleteBackdrop({
className,
...props
}: AutocompletePrimitive.Backdrop.Props) {
return (
<AutocompletePrimitive.Backdrop
className={cn(
"fixed inset-0 z-[9998] bg-black/20 dark:bg-black/40",
className
)}
data-slot="autocomplete-backdrop"
{...props}
/>
);
}
function AutocompleteContent({
align = "start",
alignOffset = 0,
anchor,
children,
className,
collisionAvoidance = autocompleteCollisionAvoidance,
collisionPadding = VIEWPORT_PADDING,
side = "bottom",
sideOffset = 6,
...props
}: AutocompletePrimitive.Popup.Props &
Pick<
AutocompletePrimitive.Positioner.Props,
| "align"
| "alignOffset"
| "anchor"
| "collisionAvoidance"
| "collisionPadding"
| "side"
| "sideOffset"
>) {
return (
<AutocompletePrimitive.Portal keepMounted>
<AutocompletePrimitive.Positioner
align={align}
alignOffset={alignOffset}
anchor={anchor}
className="z-[9999] outline-none"
collisionAvoidance={collisionAvoidance}
collisionPadding={collisionPadding}
side={side}
sideOffset={sideOffset}
>
<AutocompletePrimitive.Popup
data-slot="autocomplete-content"
initialFocus={false}
{...props}
render={(popupProps, popupState) => {
const { popupClassName, popupRef, popupStyle, resolvedPopupProps } =
resolvePopupProps(popupProps as DivRenderProps);
return (
<AutocompleteContentPanel
className={className}
popupClassName={popupClassName}
popupRef={popupRef}
popupState={popupState}
popupStyle={popupStyle}
resolvedPopupProps={resolvedPopupProps}
>
{children}
</AutocompleteContentPanel>
);
}}
/>
</AutocompletePrimitive.Positioner>
</AutocompletePrimitive.Portal>
);
}
function AutocompleteList({
className,
onPointerLeave,
...props
}: AutocompletePrimitive.List.Props) {
const { setActiveValue } = useAutocompleteContext("AutocompleteList");
return (
<ScrollAreaPrimitive.Root
className={cn("relative min-h-0 overflow-hidden", className)}
style={{
maxHeight: `min(var(--available-height, ${MAX_MENU_HEIGHT}px), ${MAX_MENU_HEIGHT}px)`,
}}
>
<AutocompletePrimitive.List
data-slot="autocomplete-list"
onPointerLeave={composeEventHandlers(onPointerLeave, () => {
setActiveValue(undefined);
})}
{...props}
render={(listProps) => {
const {
children: listChildren,
className: listClassName,
ref: listRef,
style: listStyle,
...resolvedListProps
} = listProps as DivRenderProps;
return (
<ScrollAreaPrimitive.Viewport
{...resolvedListProps}
className={cn(
"max-h-[inherit] min-h-0 overscroll-contain p-1 outline-none",
listClassName
)}
ref={(node) => {
setRef(listRef, node);
}}
style={listStyle}
>
{listChildren}
</ScrollAreaPrimitive.Viewport>
);
}}
/>
<ScrollAreaPrimitive.Scrollbar
className={autocompleteListScrollbarClassName}
orientation="vertical"
>
<ScrollAreaPrimitive.Thumb className={autocompleteListThumbClassName} />
</ScrollAreaPrimitive.Scrollbar>
</ScrollAreaPrimitive.Root>
);
}
function AutocompleteItem({
className,
children,
description,
value,
...props
}: AutocompletePrimitive.Item.Props & {
description?: React.ReactNode;
}) {
const {
activeHighlightId,
activeValue,
prefersReducedMotion,
setActiveValue,
} = useAutocompleteContext("AutocompleteItem");
return (
<AutocompletePrimitive.Item
data-slot="autocomplete-item"
value={value}
{...props}
render={(itemProps, itemState) => {
const { itemClassName, itemRef, itemStyle, resolvedItemProps } =
resolveItemProps(itemProps as DivRenderProps);
const showHighlight =
itemState.highlighted || Object.is(activeValue, value);
return (
<motion.div
{...resolvedItemProps}
animate={{ opacity: 1, y: 0 }}
className={cn(
"relative flex cursor-pointer select-none items-start px-2.5 py-2 text-foreground text-sm",
controlCornerClassName,
itemClassName,
resolveStateClassName(className, itemState)
)}
initial={prefersReducedMotion ? false : { opacity: 0, y: 2 }}
onMouseEnter={composeEventHandlers(
resolvedItemProps.onMouseEnter,
() => {
setActiveValue(value);
}
)}
onPointerMove={composeEventHandlers(
resolvedItemProps.onPointerMove,
() => {
setActiveValue(value);
}
)}
ref={(node) => {
setRef(itemRef, node);
}}
style={itemStyle}
transition={
prefersReducedMotion
? INSTANT_TRANSITION
: { duration: 0.12, ease: "easeOut" }
}
>
{showHighlight ? (
<motion.div
className={cn(
"absolute inset-0 bg-accent",
controlCornerInheritClassName
)}
layoutId={activeHighlightId}
transition={
prefersReducedMotion ? INSTANT_TRANSITION : HIGHLIGHT_SPRING
}
/>
) : null}
<span className="relative z-10 flex flex-col">
<span className="font-medium leading-tight">{children}</span>
{description ? (
<span className="text-muted-foreground text-xs">
{description}
</span>
) : null}
</span>
</motion.div>
);
}}
/>
);
}
function AutocompleteEmpty({
className,
children = "No results found.",
...props
}: AutocompletePrimitive.Empty.Props) {
const { prefersReducedMotion } = useAutocompleteContext("AutocompleteEmpty");
return (
<AutocompletePrimitive.Empty
className={cn("text-center text-muted-foreground text-sm", className)}
data-slot="autocomplete-empty"
{...props}
>
<motion.span
animate={{ opacity: 1 }}
className="block px-3 py-6"
initial={prefersReducedMotion ? false : { opacity: 0 }}
transition={
prefersReducedMotion ? INSTANT_TRANSITION : { duration: 0.18 }
}
>
{children}
</motion.span>
</AutocompletePrimitive.Empty>
);
}
function AutocompleteStatus({
className,
...props
}: AutocompletePrimitive.Status.Props) {
return (
<AutocompletePrimitive.Status
className={cn("px-3 py-2 text-muted-foreground text-xs", className)}
data-slot="autocomplete-status"
{...props}
/>
);
}
function AutocompleteRow({
className,
...props
}: AutocompletePrimitive.Row.Props) {
return (
<AutocompletePrimitive.Row
className={cn(className)}
data-slot="autocomplete-row"
{...props}
/>
);
}
function AutocompleteGroup({
className,
...props
}: AutocompletePrimitive.Group.Props) {
return (
<AutocompletePrimitive.Group
className={cn(className)}
data-slot="autocomplete-group"
{...props}
/>
);
}
function AutocompleteLabel({
className,
...props
}: AutocompletePrimitive.GroupLabel.Props) {
return (
<AutocompletePrimitive.GroupLabel
className={cn("px-2 py-1.5 text-muted-foreground text-xs", className)}
data-slot="autocomplete-label"
{...props}
/>
);
}
function AutocompleteCollection({
...props
}: AutocompletePrimitive.Collection.Props) {
return (
<AutocompletePrimitive.Collection
data-slot="autocomplete-collection"
{...props}
/>
);
}
function AutocompleteSeparator({
className,
...props
}: AutocompletePrimitive.Separator.Props) {
return (
<AutocompletePrimitive.Separator
className={cn("-mx-1 my-1 h-px bg-border", className)}
data-slot="autocomplete-separator"
{...props}
/>
);
}
function useAutocompleteAnchor() {
return React.useRef<HTMLDivElement | null>(null);
}
const useAutocompleteFilter = AutocompletePrimitive.useFilter;
const useFilteredAutocompleteItems = AutocompletePrimitive.useFilteredItems;
export {
Autocomplete,
AutocompleteBackdrop,
AutocompleteClear,
AutocompleteCollection,
AutocompleteContent,
AutocompleteEmpty,
AutocompleteGroup,
AutocompleteIcon,
AutocompleteInput,
AutocompleteItem,
AutocompleteLabel,
AutocompleteList,
AutocompleteRow,
AutocompleteSeparator,
AutocompleteStatus,
AutocompleteTrigger,
AutocompleteValue,
useAutocompleteAnchor,
useAutocompleteFilter,
useFilteredAutocompleteItems,
};