import { Button, Combobox, Form, FormControl, FormDescription, FormItem, FormLabel, FormMessage } from "@ai-ui/ui"; import type { Meta, StoryObj } from "@storybook/react"; import { Controller, useForm } from "react-hook-form"; import { useState } from "react"; const teamItems = [ { value: "design", label: "Design", group: "Primary teams", description: "Owns interface quality and review workflows.", keywords: ["ux", "ui", "visual"] }, { value: "engineering", label: "Engineering", group: "Primary teams", description: "Implements and verifies rollout mechanics.", keywords: ["dev", "build", "api"] }, { value: "legal", label: "Legal", group: "Specialist teams", description: "Checks policy, compliance, and contractual risk.", keywords: ["policy", "compliance"] }, { value: "ops", label: "Operations", group: "Specialist teams", description: "Coordinates timing, communications, and monitoring.", keywords: ["launch", "support"] } ] as const; function ControlledDemo() { const [value, setValue] = useState("design"); return (
Current value: {value}
); } function RecentAndSuggestedDemo() { const [value, setValue] = useState(""); const items = [ { value: "recent-legal", label: "Legal review", group: "Recent", description: "Last used in yesterday’s policy update." }, { value: "recent-design", label: "Design review", group: "Recent", description: "Common pick for UI launches." }, ...teamItems ]; return (
`No team named “${query}”. Create a custom routing lane instead.`} footer={

Need a specialist lane?

} items={items} onValueChange={setValue} searchPlaceholder="Search recent and suggested teams" value={value} />
Current routing lane:{" "} {value || "No lane selected"}
); } function AsyncResultsDemo() { const [searchValue, setSearchValue] = useState(""); const trimmedSearch = searchValue.trim().toLowerCase(); const isSearching = trimmedSearch.length > 0 && trimmedSearch.length < 3; return (
`No routing lane matched “${query}”. Try a broader keyword or create a new lane.` } items={[...teamItems]} loading={isSearching} loadingMessage="Searching routing lanes…" onSearchValueChange={setSearchValue} searchPlaceholder="Type at least 3 characters" searchValue={searchValue} />

This pattern is useful when the results come from an API and you need a clear transition between loading, empty, and selectable states.

); } function LaunchRoutingForm() { const [submitted, setSubmitted] = useState | null>(null); const form = useForm<{ team: string }>({ defaultValues: { team: "" } }); return (
{ setSubmitted(values); })} >

Launch routing

Combobox can live inside FormControl and surface RHF validation state.

( Routing team The chosen team becomes the primary owner for approvals and notifications. )} />

Submitted payload

            {submitted ? JSON.stringify(submitted, null, 2) : "Submit the form to inspect values."}
          
); } const meta = { title: "Components/Combobox", component: ControlledDemo, parameters: { docs: { description: { component: "Combobox is the searchable single-select surface for longer option sets, recent picks, and async lookup flows. The list should feel gently staged as results appear, with active rows gliding into place instead of snapping." } }, layout: "centered" }, tags: ["autodocs"] } satisfies Meta; export default meta; type Story = StoryObj; export const Controlled: Story = { render: () => }; export const RecentAndSuggested: Story = { render: () => }; export const AsyncResults: Story = { parameters: { docs: { description: { story: "Async comboboxes should crossfade between loading, empty, and result states. Keep the motion short and directional so the user understands the state change without losing the current context." } } }, render: () => }; export const WithForm: Story = { render: () => };