Item picker
Show a list and resolve with the chosen item. Caller-side cancellation returns null; selecting an item returns the object itself.
→ nothing picked yet
The Callable
declared once, mounted in the React treeimport { createCallable } from 'react-call'
interface Item { id: string name: string hint?: string}
interface Props { title: string items: readonly Item[]}
export const Picker = createCallable<Props, Item | null>( ({ call, title, items }) => ( <div role="dialog" aria-modal="true" aria-label={title} className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm" > <div className="w-full max-w-sm rounded-lg border border-[var(--color-border)] bg-[var(--color-bg)] p-2 shadow-2xl"> <div className="flex items-center justify-between px-3 py-2"> <p className="text-sm font-medium text-[var(--color-fg)]">{title}</p> <button type="button" onClick={() => call.end(null)} aria-label="Cancel" className="-mr-1 inline-flex h-7 w-7 items-center justify-center rounded-md text-base leading-none text-[var(--color-fg-subtle)] transition-colors hover:bg-[var(--color-bg-subtle)] hover:text-[var(--color-fg)]" > × </button> </div> <ul className="max-h-72 overflow-y-auto"> {items.map((item) => ( <li key={item.id}> <button type="button" onClick={() => call.end(item)} className="flex w-full items-center justify-between gap-3 rounded-md px-3 py-2 text-left text-sm text-[var(--color-fg)] transition-colors hover:bg-[var(--color-bg-subtle)]" > <span>{item.name}</span> {item.hint && ( <span className="font-mono text-xs text-[var(--color-fg-subtle)]"> {item.hint} </span> )} </button> </li> ))} </ul> </div> </div> ),)Picker.displayName = 'Picker'The Root
mounted in your app tree, onceimport { Picker } from './Picker'import { FruitPickerTrigger } from './FruitPickerTrigger'
export default function App() { return ( <> <Picker /> <FruitPickerTrigger /> </> )}The caller
anywhere in your app, imperativeimport { useState } from 'react'import { Picker } from './Picker'
const FRUITS = [ { id: 'apple', name: 'Apple', hint: '🍎' }, { id: 'banana', name: 'Banana', hint: '🍌' }, { id: 'cherry', name: 'Cherry', hint: '🍒' }, { id: 'grape', name: 'Grape', hint: '🍇' }, { id: 'mango', name: 'Mango', hint: '🥭' },] as const
export const FruitPickerTrigger = () => { const [picked, setPicked] = useState<string | null>(null)
const handleClick = async () => { const choice = await Picker.call({ title: 'Pick a fruit', items: FRUITS, }) setPicked(choice ? choice.name : null) }
return ( <div className="flex flex-col items-center gap-3"> <button type="button" onClick={handleClick} className="rounded-md bg-[var(--color-accent)] px-4 py-2 text-sm font-medium text-[var(--color-accent-fg)] transition-colors hover:bg-[var(--color-accent-hover)]" > Pick a fruit </button> <span className="font-mono text-xs text-[var(--color-fg-subtle)]"> {picked ? ( <span className="text-[var(--color-accent)]">→ {picked}</span> ) : ( '→ nothing picked yet' )} </span> </div> )}