Vela UI Docsvenizia.ai

Combobox

Autocomplete input with a filterable list of suggestions, single or multi-select.

npx shadcn@latest add @velaui/combobox
npx shadcn@latest add @velaui/combobox

Examples

  • Default
  • Fill medium
  • Underline large
  • Disabled
  • Invalid
  • Grouped
  • Multiple

View code

Combobox · Default

Installation

pnpm dlx velaui add @velaui/combobox
pnpm dlx velaui add @velaui/combobox

Code

combobox-default.tsx
import {
  Combobox,
  ComboboxCollection,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
} from "@/components/ui/combobox";

const fruits = ["Apple", "Banana", "Blueberry", "Grapes", "Pineapple"];

export default function Example() {
  return (
    <Combobox items={fruits}>
      <ComboboxInput className="w-[220px]" placeholder="Search a fruit" />
      <ComboboxContent>
        <ComboboxEmpty>No fruit found.</ComboboxEmpty>
        <ComboboxList>
          <ComboboxCollection>
            {(item: string) => (
              <ComboboxItem key={item} value={item}>
                {item}
              </ComboboxItem>
            )}
          </ComboboxCollection>
        </ComboboxList>
      </ComboboxContent>
    </Combobox>
  );
}

View code

Combobox · Fill medium

Installation

pnpm dlx velaui add @velaui/combobox
pnpm dlx velaui add @velaui/combobox

Code

combobox-fill-medium.tsx
import {
  Combobox,
  ComboboxCollection,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
} from "@/components/ui/combobox";

const languages = [
  "TypeScript",
  "JavaScript",
  "Go",
  "Rust",
  "Python",
  "Elixir",
];

export default function Example() {
  return (
    <Combobox items={languages} defaultValue="TypeScript">
      <ComboboxInput
        variant="fill"
        size="md"
        className="w-[220px]"
        placeholder="Pick a language"
        showClear
      />
      <ComboboxContent>
        <ComboboxEmpty>No language found.</ComboboxEmpty>
        <ComboboxList>
          <ComboboxCollection>
            {(item: string) => (
              <ComboboxItem key={item} value={item}>
                {item}
              </ComboboxItem>
            )}
          </ComboboxCollection>
        </ComboboxList>
      </ComboboxContent>
    </Combobox>
  );
}

View code

Combobox · Underline large

Installation

pnpm dlx velaui add @velaui/combobox
pnpm dlx velaui add @velaui/combobox

Code

combobox-underline-large.tsx
import {
  Combobox,
  ComboboxCollection,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
} from "@/components/ui/combobox";

const regions = ["ap-southeast-1", "ap-northeast-1", "eu-west-1", "us-east-1"];

export default function Example() {
  return (
    <Combobox items={regions}>
      <ComboboxInput
        variant="underline"
        size="lg"
        className="w-[240px]"
        placeholder="Region"
      />
      <ComboboxContent>
        <ComboboxEmpty>No region found.</ComboboxEmpty>
        <ComboboxList>
          <ComboboxCollection>
            {(item: string) => (
              <ComboboxItem key={item} value={item}>
                {item}
              </ComboboxItem>
            )}
          </ComboboxCollection>
        </ComboboxList>
      </ComboboxContent>
    </Combobox>
  );
}

View code

Combobox · Disabled

Installation

pnpm dlx velaui add @velaui/combobox
pnpm dlx velaui add @velaui/combobox

Code

combobox-disabled.tsx
import { Combobox, ComboboxInput } from "@/components/ui/combobox";

const regions = ["ap-southeast-1", "ap-northeast-1", "eu-west-1", "us-east-1"];

export default function Example() {
  return (
    <Combobox items={regions} defaultValue="ap-southeast-1" disabled>
      <ComboboxInput className="w-[240px]" disabled placeholder="Region" />
    </Combobox>
  );
}

View code

Combobox · Invalid

Installation

pnpm dlx velaui add @velaui/combobox
pnpm dlx velaui add @velaui/combobox

Code

combobox-invalid.tsx
import { Label } from "@/components/ui/label";
import {
  Combobox,
  ComboboxCollection,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
} from "@/components/ui/combobox";

const countries = ["Vietnam", "United States", "Japan", "Germany"];

export default function Example() {
  return (
    <div className="flex w-[240px] flex-col gap-2">
      <Label htmlFor="country">Country</Label>
      <Combobox items={countries}>
        <ComboboxInput
          id="country"
          aria-invalid
          className="w-full"
          placeholder="Select a country"
        />
        <ComboboxContent>
          <ComboboxEmpty>No country found.</ComboboxEmpty>
          <ComboboxList>
            <ComboboxCollection>
              {(item: string) => (
                <ComboboxItem key={item} value={item}>
                  {item}
                </ComboboxItem>
              )}
            </ComboboxCollection>
          </ComboboxList>
        </ComboboxContent>
      </Combobox>
      <p className="text-negative-middle text-sm">Please select a country.</p>
    </div>
  );
}

View code

Combobox · Grouped

Installation

pnpm dlx velaui add @velaui/combobox
pnpm dlx velaui add @velaui/combobox

Code

combobox-grouped.tsx
import {
  Combobox,
  ComboboxCollection,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxGroup,
  ComboboxInput,
  ComboboxItem,
  ComboboxLabel,
  ComboboxList,
} from "@/components/ui/combobox";

const groups = [
  { label: "Fruits", items: ["Apple", "Banana", "Blueberry"] },
  { label: "Vegetables", items: ["Carrot", "Potato"] },
];

export default function Example() {
  return (
    <Combobox items={groups}>
      <ComboboxInput className="w-[220px]" placeholder="Search produce" />
      <ComboboxContent>
        <ComboboxEmpty>No produce found.</ComboboxEmpty>
        <ComboboxList>
          <ComboboxCollection>
            {(group: (typeof groups)[number]) => (
              <ComboboxGroup key={group.label} items={group.items}>
                <ComboboxLabel>{group.label}</ComboboxLabel>
                {group.items.map((item) => (
                  <ComboboxItem key={item} value={item}>
                    {item}
                  </ComboboxItem>
                ))}
              </ComboboxGroup>
            )}
          </ComboboxCollection>
        </ComboboxList>
      </ComboboxContent>
    </Combobox>
  );
}

View code

Combobox · Multiple

Installation

pnpm dlx velaui add @velaui/combobox
pnpm dlx velaui add @velaui/combobox

Code

combobox-multiple.tsx
import { Fragment } from "react/jsx-runtime";
import {
  Combobox,
  ComboboxChip,
  ComboboxChips,
  ComboboxChipsInput,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxItem,
  ComboboxList,
  ComboboxValue,
  useComboboxAnchor,
} from "@/components/ui/combobox";
import { Label } from "@/components/ui/label";

const fruits = ["Apple", "Banana", "Blueberry", "Grapes", "Pineapple"];

export default function ComboboxMultiple() {
  const anchor = useComboboxAnchor();

  return (
    <div className="space-y-2">
      <Label>Multiple</Label>
      <Combobox
        multiple
        autoHighlight
        items={fruits}
        defaultValue={[fruits[0]]}
      >
        <ComboboxChips ref={anchor} className="w-full max-w-xs">
          <ComboboxValue>
            {(values) => (
              <Fragment>
                {values.map((value: string) => (
                  <ComboboxChip key={value}>{value}</ComboboxChip>
                ))}
                <ComboboxChipsInput placeholder="Select fruits" />
              </Fragment>
            )}
          </ComboboxValue>
        </ComboboxChips>
        <ComboboxContent anchor={anchor}>
          <ComboboxEmpty>No items found.</ComboboxEmpty>
          <ComboboxList>
            {(item) => (
              <ComboboxItem key={item} value={item}>
                {item}
              </ComboboxItem>
            )}
          </ComboboxList>
        </ComboboxContent>
      </Combobox>
    </div>
  );
}

Usage

import {
  Combobox,
  ComboboxCollection,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
} from "@/components/ui/combobox";

const fruits = ["Apple", "Banana", "Blueberry", "Grapes", "Pineapple"];

export default function Example() {
  return (
    <Combobox items={fruits}>
      <ComboboxInput className="w-[220px]" placeholder="Search a fruit" />
      <ComboboxContent>
        <ComboboxEmpty>No fruit found.</ComboboxEmpty>
        <ComboboxList>
          <ComboboxCollection>
            {(item: string) => (
              <ComboboxItem key={item} value={item}>
                {item}
              </ComboboxItem>
            )}
          </ComboboxCollection>
        </ComboboxList>
      </ComboboxContent>
    </Combobox>
  );
}

Props

ComboboxInput

PropTypeDefault
disabledbooleanfalse
showClearbooleanfalse
showTriggerbooleantrue
size"sm" | "md" | "lg""sm"
variant"fill" | "underline" | "outlined""outlined"

Takes these and every prop of ComboboxInputProps.

ComboboxContent

PropTypeDefault
alignimport("/app/node_modules/.bun/@base-ui+react@1.6.0+df7f6a806acd27ca/node_modules/@base-ui/react/utils/useAnchorPositioning").Align"start"
alignOffsetnumber | import("/app/node_modules/.bun/@base-ui+react@1.6.0+df7f6a806acd27ca/node_modules/@base-ui/react/utils/useAnchorPositioning").OffsetFunction0
sideimport("/app/node_modules/.bun/@base-ui+react@1.6.0+df7f6a806acd27ca/node_modules/@base-ui/react/utils/useAnchorPositioning").Side"bottom"
sideOffsetnumber | import("/app/node_modules/.bun/@base-ui+react@1.6.0+df7f6a806acd27ca/node_modules/@base-ui/react/utils/useAnchorPositioning").OffsetFunction4

Takes these and every prop of ComboboxContentProps.

ComboboxList

Takes every prop of ComboboxListProps.

ComboboxItem

Takes every prop of ComboboxItemProps.

ComboboxGroup

Takes every prop of ComboboxGroupProps.

ComboboxLabel

Takes every prop of ComboboxLabelProps.

ComboboxCollection

Takes every prop of ComboboxCollectionProps.

ComboboxEmpty

Takes every prop of ComboboxEmptyProps.

ComboboxSeparator

Takes every prop of ComboboxSeparatorProps.

ComboboxChips

Takes every prop of ComboboxChipsProps.

ComboboxChip

PropTypeDefault
showRemovebooleantrue

Takes these and every prop of ComboboxChipProps.

ComboboxChipsInput

Takes every prop of ComboboxChipsInputProps.

ComboboxTrigger

Takes every prop of ComboboxTriggerProps.

ComboboxValue

Takes every prop of ComboboxValueProps.

ComboboxInput

PropTypeDefault
disabledbooleanfalse
showClearbooleanfalse
showTriggerbooleantrue
size"sm" | "md" | "lg""sm"
variant"underline" | "fill" | "outlined""outlined"

Takes these and every prop of ComboboxInputProps.

ComboboxContent

PropTypeDefault
alignimport("/app/node_modules/.bun/@base-ui+react@1.6.0+df7f6a806acd27ca/node_modules/@base-ui/react/utils/useAnchorPositioning").Align"start"
alignOffsetnumber | import("/app/node_modules/.bun/@base-ui+react@1.6.0+df7f6a806acd27ca/node_modules/@base-ui/react/utils/useAnchorPositioning").OffsetFunction0
sideimport("/app/node_modules/.bun/@base-ui+react@1.6.0+df7f6a806acd27ca/node_modules/@base-ui/react/utils/useAnchorPositioning").Side"bottom"
sideOffsetnumber | import("/app/node_modules/.bun/@base-ui+react@1.6.0+df7f6a806acd27ca/node_modules/@base-ui/react/utils/useAnchorPositioning").OffsetFunction4

Takes these and every prop of ComboboxContentProps.

ComboboxList

Takes every prop of ComboboxListProps.

ComboboxItem

Takes every prop of ComboboxItemProps.

ComboboxGroup

Takes every prop of ComboboxGroupProps.

ComboboxLabel

Takes every prop of ComboboxLabelProps.

ComboboxCollection

Takes every prop of ComboboxCollectionProps.

ComboboxEmpty

Takes every prop of ComboboxEmptyProps.

ComboboxSeparator

Takes every prop of ComboboxSeparatorProps.

ComboboxChips

Takes every prop of ComboboxChipsProps.

ComboboxChip

PropTypeDefault
showRemovebooleantrue

Takes these and every prop of ComboboxChipProps.

ComboboxChipsInput

Takes every prop of ComboboxChipsInputProps.

ComboboxTrigger

Takes every prop of ComboboxTriggerProps.

ComboboxValue

Takes every prop of ComboboxValueProps.

Dependencies

npm
@base-ui/react@1.6.0class-variance-authority@0.7.1
Installs with it
ButtonInput Group3 icons

Source

components/ui/combobox.tsx
/* eslint-disable react-refresh/only-export-components */
"use client";

import * as React from "react";
import { Combobox as ComboboxPrimitive } from "@base-ui/react";
import { type VariantProps } from "class-variance-authority";
import { Check01 } from "@/components/icons/check-01";
import { ChevronDown } from "@/components/icons/chevron-down";
import { Close02 } from "@/components/icons/close-02";

import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import {
  InputGroup,
  InputGroupAddon,
  InputGroupButton,
  InputGroupInput,
  inputGroupVariants,
} from "@/components/ui/input-group";

export type ComboboxProps = React.ComponentProps<typeof ComboboxPrimitive.Root>;

const Combobox = ComboboxPrimitive.Root;

export type ComboboxValueProps = ComboboxPrimitive.Value.Props;

function ComboboxValue({ ...props }: ComboboxValueProps) {
  return <ComboboxPrimitive.Value data-slot="combobox-value" {...props} />;
}

export type ComboboxTriggerProps = ComboboxPrimitive.Trigger.Props;

function ComboboxTrigger({
  className,
  children,
  ...props
}: ComboboxTriggerProps) {
  return (
    <ComboboxPrimitive.Trigger
      data-slot="combobox-trigger"
      className={cn(
        // Open flips the chevron — the only thing the trigger itself restyles.
        "text-icon-muted data-popup-open:[&>svg]:rotate-180 [&_svg:not([class*='size-'])]:size-4",
        className,
      )}
      {...props}
    >
      {children}
      <ChevronDown className="pointer-events-none size-4 text-icon-muted transition-transform" />
    </ComboboxPrimitive.Trigger>
  );
}

function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) {
  return (
    <ComboboxPrimitive.Clear
      data-slot="combobox-clear"
      render={<InputGroupButton variant="ghost" size="icon-xs" />}
      className={cn("text-icon-muted", className)}
      {...props}
    >
      <Close02 className="pointer-events-none" />
    </ComboboxPrimitive.Clear>
  );
}

export type ComboboxInputProps = Omit<ComboboxPrimitive.Input.Props, "size"> & {
  showTrigger?: boolean;
  showClear?: boolean;
} & VariantProps<typeof inputGroupVariants>;

function ComboboxInput({
  className,
  children,
  disabled = false,
  showTrigger = true,
  showClear = false,
  variant = "outlined",
  size = "sm",
  ...props
}: ComboboxInputProps) {
  return (
    <InputGroup
      variant={variant}
      size={size}
      className={cn("w-auto", className)}
    >
      <ComboboxPrimitive.Input
        render={<InputGroupInput disabled={disabled} />}
        {...props}
      />
      <InputGroupAddon align="inline-end">
        {showTrigger && (
          <InputGroupButton
            size="icon-xs"
            variant="ghost"
            asChild
            data-slot="input-group-button"
            className="group-has-data-[slot=combobox-clear]/input-group:hidden data-pressed:bg-transparent"
            disabled={disabled}
          >
            <ComboboxTrigger />
          </InputGroupButton>
        )}
        {showClear && <ComboboxClear disabled={disabled} />}
      </InputGroupAddon>
      {children}
    </InputGroup>
  );
}

export type ComboboxContentProps = ComboboxPrimitive.Popup.Props &
  Pick<
    ComboboxPrimitive.Positioner.Props,
    "side" | "align" | "sideOffset" | "alignOffset" | "anchor"
  >;

function ComboboxContent({
  className,
  side = "bottom",
  sideOffset = 4,
  align = "start",
  alignOffset = 0,
  anchor,
  ...props
}: ComboboxContentProps) {
  return (
    <ComboboxPrimitive.Portal>
      <ComboboxPrimitive.Positioner
        side={side}
        sideOffset={sideOffset}
        align={align}
        alignOffset={alignOffset}
        anchor={anchor}
        className="isolate z-50"
      >
        <ComboboxPrimitive.Popup
          data-slot="combobox-content"
          data-chips={!!anchor}
          className={cn(
            "group/combobox-content relative max-h-(--available-height) w-(--anchor-width) max-w-(--available-width) min-w-[calc(var(--anchor-width)+--spacing(7))] origin-(--transform-origin) overflow-hidden rounded-md border border-border-default bg-bg-main text-text-strong shadow-md duration-100 data-[chips=true]:min-w-(--anchor-width) data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 *:data-[slot=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-8 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
            className,
          )}
          {...props}
        />
      </ComboboxPrimitive.Positioner>
    </ComboboxPrimitive.Portal>
  );
}

export type ComboboxListProps = ComboboxPrimitive.List.Props;

function ComboboxList({ className, ...props }: ComboboxListProps) {
  return (
    <ComboboxPrimitive.List
      data-slot="combobox-list"
      className={cn(
        "no-scrollbar flex max-h-[min(calc(--spacing(72)---spacing(9)),calc(var(--available-height)---spacing(9)))] scroll-py-1 flex-col gap-1 overflow-y-auto overscroll-contain p-1 data-empty:p-0",
        className,
      )}
      {...props}
    />
  );
}

export type ComboboxItemProps = ComboboxPrimitive.Item.Props;

function ComboboxItem({
  className,
  children,
  ...props
}: ComboboxItemProps) {
  return (
    <ComboboxPrimitive.Item
      data-slot="combobox-item"
      className={cn(
        "relative flex w-full cursor-default items-center gap-2 rounded-md py-1.5 pr-10 pl-2 text-sm leading-[1.4] text-text-strong outline-hidden select-none data-highlighted:bg-bg-surface-1 data-selected:bg-bg-surface-1 data-disabled:pointer-events-none data-disabled:text-text-disabled [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-5",
        className,
      )}
      {...props}
    >
      {children}
      <ComboboxPrimitive.ItemIndicator
        render={
          <span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" />
        }
      >
        <Check01 className="pointer-events-none size-4 text-icon-strong" />
      </ComboboxPrimitive.ItemIndicator>
    </ComboboxPrimitive.Item>
  );
}

export type ComboboxGroupProps = ComboboxPrimitive.Group.Props;

function ComboboxGroup({ className, ...props }: ComboboxGroupProps) {
  return (
    <ComboboxPrimitive.Group
      data-slot="combobox-group"
      className={cn("flex scroll-my-1 flex-col gap-1", className)}
      {...props}
    />
  );
}

export type ComboboxLabelProps = ComboboxPrimitive.GroupLabel.Props;

function ComboboxLabel({
  className,
  ...props
}: ComboboxLabelProps) {
  return (
    <ComboboxPrimitive.GroupLabel
      data-slot="combobox-label"
      className={cn("px-2 py-1 text-xs text-text-muted", className)}
      {...props}
    />
  );
}

export type ComboboxCollectionProps = ComboboxPrimitive.Collection.Props;

function ComboboxCollection({ ...props }: ComboboxCollectionProps) {
  return (
    <ComboboxPrimitive.Collection data-slot="combobox-collection" {...props} />
  );
}

export type ComboboxEmptyProps = ComboboxPrimitive.Empty.Props;

function ComboboxEmpty({ className, ...props }: ComboboxEmptyProps) {
  return (
    <ComboboxPrimitive.Empty
      data-slot="combobox-empty"
      className={cn(
        "hidden w-full justify-center py-2 text-center text-sm text-text-muted group-data-empty/combobox-content:flex",
        className,
      )}
      {...props}
    />
  );
}

export type ComboboxSeparatorProps = ComboboxPrimitive.Separator.Props;

function ComboboxSeparator({
  className,
  ...props
}: ComboboxSeparatorProps) {
  return (
    <ComboboxPrimitive.Separator
      data-slot="combobox-separator"
      className={cn(
        "pointer-events-none -mx-1 my-1 h-px bg-border-default",
        className,
      )}
      {...props}
    />
  );
}

export type ComboboxChipsProps = React.ComponentPropsWithRef<typeof ComboboxPrimitive.Chips> &
  ComboboxPrimitive.Chips.Props;

function ComboboxChips({
  className,
  ...props
}: ComboboxChipsProps) {
  return (
    <ComboboxPrimitive.Chips
      data-slot="combobox-chips"
      className={cn(
        "flex min-h-8 flex-wrap items-center gap-1 rounded-md border border-border-default bg-bg-main bg-clip-padding px-2.5 py-1 text-sm leading-[1.4] text-text-strong transition-colors focus-within:border-ring-neutral-outline focus-within:ring-3 focus-within:ring-ring-neutral-shadow has-aria-invalid:border-negative-middle has-aria-invalid:ring-3 has-aria-invalid:ring-negative-middle/25 has-data-[slot=combobox-chip]:px-1",
        className,
      )}
      {...props}
    />
  );
}

export type ComboboxChipProps = ComboboxPrimitive.Chip.Props & {
  showRemove?: boolean;
};

function ComboboxChip({
  className,
  children,
  showRemove = true,
  ...props
}: ComboboxChipProps) {
  return (
    <ComboboxPrimitive.Chip
      data-slot="combobox-chip"
      className={cn(
        "flex h-[calc(--spacing(5.25))] w-fit items-center justify-center gap-1 rounded-sm bg-bg-surface-1 px-1.5 text-xs font-medium whitespace-nowrap text-text-strong has-disabled:pointer-events-none has-disabled:cursor-not-allowed has-disabled:text-text-disabled has-data-[slot=combobox-chip-remove]:pr-0",
        className,
      )}
      {...props}
    >
      {children}
      {showRemove && (
        <ComboboxPrimitive.ChipRemove
          render={<Button variant="ghost" size="icon-xs" />}
          className="-ml-1 text-icon-muted opacity-50 hover:opacity-100 hover:bg-transparent"
          data-slot="combobox-chip-remove"
        >
          <Close02 className="pointer-events-none" />
        </ComboboxPrimitive.ChipRemove>
      )}
    </ComboboxPrimitive.Chip>
  );
}

export type ComboboxChipsInputProps = ComboboxPrimitive.Input.Props;

function ComboboxChipsInput({
  className,
  ...props
}: ComboboxChipsInputProps) {
  return (
    <ComboboxPrimitive.Input
      data-slot="combobox-chip-input"
      className={cn(
        "min-w-16 flex-1 text-sm leading-[1.4] text-text-strong outline-none placeholder:text-text-note",
        className,
      )}
      {...props}
    />
  );
}

function useComboboxAnchor() {
  return React.useRef<HTMLDivElement | null>(null);
}

export {
  Combobox,
  ComboboxInput,
  ComboboxContent,
  ComboboxList,
  ComboboxItem,
  ComboboxGroup,
  ComboboxLabel,
  ComboboxCollection,
  ComboboxEmpty,
  ComboboxSeparator,
  ComboboxChips,
  ComboboxChip,
  ComboboxChipsInput,
  ComboboxTrigger,
  ComboboxValue,
  useComboboxAnchor,
};
components/ui/combobox.tsx
/* eslint-disable react-refresh/only-export-components */
"use client";

import * as React from "react";
import { Combobox as ComboboxPrimitive } from "@base-ui/react";
import { type VariantProps } from "class-variance-authority";
import { Check01 } from "@/components/icons/check-01";
import { ChevronDown } from "@/components/icons/chevron-down";
import { Close02 } from "@/components/icons/close-02";

import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import {
  InputGroup,
  InputGroupAddon,
  InputGroupButton,
  InputGroupInput,
  inputGroupVariants,
} from "@/components/ui/input-group";

export type ComboboxProps = React.ComponentProps<typeof ComboboxPrimitive.Root>;

const Combobox = ComboboxPrimitive.Root;

export type ComboboxValueProps = ComboboxPrimitive.Value.Props;

function ComboboxValue({ ...props }: ComboboxValueProps) {
  return <ComboboxPrimitive.Value data-slot="combobox-value" {...props} />;
}

export type ComboboxTriggerProps = ComboboxPrimitive.Trigger.Props;

function ComboboxTrigger({
  className,
  children,
  ...props
}: ComboboxTriggerProps) {
  return (
    <ComboboxPrimitive.Trigger
      data-slot="combobox-trigger"
      className={cn(
        // Open flips the chevron — the only thing the trigger itself restyles.
        "text-icon-muted data-popup-open:[&>svg]:rotate-180 [&_svg:not([class*='size-'])]:size-4",
        className,
      )}
      {...props}
    >
      {children}
      <ChevronDown className="pointer-events-none size-4 text-icon-muted transition-transform" />
    </ComboboxPrimitive.Trigger>
  );
}

function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) {
  return (
    <ComboboxPrimitive.Clear
      data-slot="combobox-clear"
      render={<InputGroupButton variant="ghost" size="icon-xs" />}
      className={cn("text-icon-muted", className)}
      {...props}
    >
      <Close02 className="pointer-events-none" />
    </ComboboxPrimitive.Clear>
  );
}

export type ComboboxInputProps = Omit<ComboboxPrimitive.Input.Props, "size"> & {
  showTrigger?: boolean;
  showClear?: boolean;
} & VariantProps<typeof inputGroupVariants>;

function ComboboxInput({
  className,
  children,
  disabled = false,
  showTrigger = true,
  showClear = false,
  variant = "outlined",
  size = "sm",
  ...props
}: ComboboxInputProps) {
  return (
    <InputGroup
      variant={variant}
      size={size}
      className={cn("w-auto", className)}
    >
      <ComboboxPrimitive.Input
        render={<InputGroupInput disabled={disabled} />}
        {...props}
      />
      <InputGroupAddon align="inline-end">
        {showTrigger && (
          <InputGroupButton
            size="icon-xs"
            variant="ghost"
            render={<ComboboxTrigger />}
            data-slot="input-group-button"
            className="group-has-data-[slot=combobox-clear]/input-group:hidden data-pressed:bg-transparent"
            disabled={disabled}
          />
        )}
        {showClear && <ComboboxClear disabled={disabled} />}
      </InputGroupAddon>
      {children}
    </InputGroup>
  );
}

export type ComboboxContentProps = ComboboxPrimitive.Popup.Props &
  Pick<
    ComboboxPrimitive.Positioner.Props,
    "side" | "align" | "sideOffset" | "alignOffset" | "anchor"
  >;

function ComboboxContent({
  className,
  side = "bottom",
  sideOffset = 4,
  align = "start",
  alignOffset = 0,
  anchor,
  ...props
}: ComboboxContentProps) {
  return (
    <ComboboxPrimitive.Portal>
      <ComboboxPrimitive.Positioner
        side={side}
        sideOffset={sideOffset}
        align={align}
        alignOffset={alignOffset}
        anchor={anchor}
        className="isolate z-50"
      >
        <ComboboxPrimitive.Popup
          data-slot="combobox-content"
          data-chips={!!anchor}
          className={cn(
            "group/combobox-content relative max-h-(--available-height) w-(--anchor-width) max-w-(--available-width) min-w-[calc(var(--anchor-width)+--spacing(7))] origin-(--transform-origin) overflow-hidden rounded-md border border-border-default bg-bg-main text-text-strong shadow-md duration-100 data-[chips=true]:min-w-(--anchor-width) data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 *:data-[slot=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-8 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
            className,
          )}
          {...props}
        />
      </ComboboxPrimitive.Positioner>
    </ComboboxPrimitive.Portal>
  );
}

export type ComboboxListProps = ComboboxPrimitive.List.Props;

function ComboboxList({ className, ...props }: ComboboxListProps) {
  return (
    <ComboboxPrimitive.List
      data-slot="combobox-list"
      className={cn(
        "no-scrollbar flex max-h-[min(calc(--spacing(72)---spacing(9)),calc(var(--available-height)---spacing(9)))] scroll-py-1 flex-col gap-1 overflow-y-auto overscroll-contain p-1 data-empty:p-0",
        className,
      )}
      {...props}
    />
  );
}

export type ComboboxItemProps = ComboboxPrimitive.Item.Props;

function ComboboxItem({
  className,
  children,
  ...props
}: ComboboxItemProps) {
  return (
    <ComboboxPrimitive.Item
      data-slot="combobox-item"
      className={cn(
        "relative flex w-full cursor-default items-center gap-2 rounded-md py-1.5 pr-10 pl-2 text-sm leading-[1.4] text-text-strong outline-hidden select-none data-highlighted:bg-bg-surface-1 data-selected:bg-bg-surface-1 data-disabled:pointer-events-none data-disabled:text-text-disabled [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-5",
        className,
      )}
      {...props}
    >
      {children}
      <ComboboxPrimitive.ItemIndicator
        render={
          <span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" />
        }
      >
        <Check01 className="pointer-events-none size-4 text-icon-strong" />
      </ComboboxPrimitive.ItemIndicator>
    </ComboboxPrimitive.Item>
  );
}

export type ComboboxGroupProps = ComboboxPrimitive.Group.Props;

function ComboboxGroup({ className, ...props }: ComboboxGroupProps) {
  return (
    <ComboboxPrimitive.Group
      data-slot="combobox-group"
      className={cn("flex scroll-my-1 flex-col gap-1", className)}
      {...props}
    />
  );
}

export type ComboboxLabelProps = ComboboxPrimitive.GroupLabel.Props;

function ComboboxLabel({
  className,
  ...props
}: ComboboxLabelProps) {
  return (
    <ComboboxPrimitive.GroupLabel
      data-slot="combobox-label"
      className={cn("px-2 py-1 text-xs text-text-muted", className)}
      {...props}
    />
  );
}

export type ComboboxCollectionProps = ComboboxPrimitive.Collection.Props;

function ComboboxCollection({ ...props }: ComboboxCollectionProps) {
  return (
    <ComboboxPrimitive.Collection data-slot="combobox-collection" {...props} />
  );
}

export type ComboboxEmptyProps = ComboboxPrimitive.Empty.Props;

function ComboboxEmpty({ className, ...props }: ComboboxEmptyProps) {
  return (
    <ComboboxPrimitive.Empty
      data-slot="combobox-empty"
      className={cn(
        "hidden w-full justify-center py-2 text-center text-sm text-text-muted group-data-empty/combobox-content:flex",
        className,
      )}
      {...props}
    />
  );
}

export type ComboboxSeparatorProps = ComboboxPrimitive.Separator.Props;

function ComboboxSeparator({
  className,
  ...props
}: ComboboxSeparatorProps) {
  return (
    <ComboboxPrimitive.Separator
      data-slot="combobox-separator"
      className={cn(
        "pointer-events-none -mx-1 my-1 h-px bg-border-default",
        className,
      )}
      {...props}
    />
  );
}

export type ComboboxChipsProps = React.ComponentPropsWithRef<typeof ComboboxPrimitive.Chips> &
  ComboboxPrimitive.Chips.Props;

function ComboboxChips({
  className,
  ...props
}: ComboboxChipsProps) {
  return (
    <ComboboxPrimitive.Chips
      data-slot="combobox-chips"
      className={cn(
        "flex min-h-8 flex-wrap items-center gap-1 rounded-md border border-border-default bg-bg-main bg-clip-padding px-2.5 py-1 text-sm leading-[1.4] text-text-strong transition-colors focus-within:border-ring-neutral-outline focus-within:ring-3 focus-within:ring-ring-neutral-shadow has-aria-invalid:border-negative-middle has-aria-invalid:ring-3 has-aria-invalid:ring-negative-middle/25 has-data-[slot=combobox-chip]:px-1",
        className,
      )}
      {...props}
    />
  );
}

export type ComboboxChipProps = ComboboxPrimitive.Chip.Props & {
  showRemove?: boolean;
};

function ComboboxChip({
  className,
  children,
  showRemove = true,
  ...props
}: ComboboxChipProps) {
  return (
    <ComboboxPrimitive.Chip
      data-slot="combobox-chip"
      className={cn(
        "flex h-[calc(--spacing(5.25))] w-fit items-center justify-center gap-1 rounded-sm bg-bg-surface-1 px-1.5 text-xs font-medium whitespace-nowrap text-text-strong has-disabled:pointer-events-none has-disabled:cursor-not-allowed has-disabled:text-text-disabled has-data-[slot=combobox-chip-remove]:pr-0",
        className,
      )}
      {...props}
    >
      {children}
      {showRemove && (
        <ComboboxPrimitive.ChipRemove
          render={<Button variant="ghost" size="icon-xs" />}
          className="-ml-1 text-icon-muted opacity-50 hover:opacity-100 hover:bg-transparent"
          data-slot="combobox-chip-remove"
        >
          <Close02 className="pointer-events-none" />
        </ComboboxPrimitive.ChipRemove>
      )}
    </ComboboxPrimitive.Chip>
  );
}

export type ComboboxChipsInputProps = ComboboxPrimitive.Input.Props;

function ComboboxChipsInput({
  className,
  ...props
}: ComboboxChipsInputProps) {
  return (
    <ComboboxPrimitive.Input
      data-slot="combobox-chip-input"
      className={cn(
        "min-w-16 flex-1 text-sm leading-[1.4] text-text-strong outline-none placeholder:text-text-note",
        className,
      )}
      {...props}
    />
  );
}

function useComboboxAnchor() {
  return React.useRef<HTMLDivElement | null>(null);
}

export {
  Combobox,
  ComboboxInput,
  ComboboxContent,
  ComboboxList,
  ComboboxItem,
  ComboboxGroup,
  ComboboxLabel,
  ComboboxCollection,
  ComboboxEmpty,
  ComboboxSeparator,
  ComboboxChips,
  ComboboxChip,
  ComboboxChipsInput,
  ComboboxTrigger,
  ComboboxValue,
  useComboboxAnchor,
};