{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "picker-sheet",
  "title": "Picker Sheet",
  "description": "A select for touch screens: a button that shows the choice and opens a bottom sheet with large rows, optional search and single or multiple selection.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "@tealmac/bottom-sheet"
  ],
  "files": [
    {
      "path": "registry/tealmac/picker-sheet.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { CheckIcon, ChevronDownIcon, SearchIcon } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport {\n  BottomSheet,\n  BottomSheetClose,\n  BottomSheetContent,\n  BottomSheetDescription,\n  BottomSheetHeader,\n  BottomSheetTitle,\n  BottomSheetTrigger,\n} from \"@/registry/tealmac/bottom-sheet\"\n\nexport interface PickerOption {\n  value: string\n  label: string\n  description?: string\n  icon?: React.ReactNode\n  disabled?: boolean\n}\n\ninterface PickerBase {\n  options: PickerOption[]\n  /** Title of the sheet and accessible name of the list. */\n  title: string\n  placeholder?: string\n  /** Add a search field above the list. */\n  searchable?: boolean\n  className?: string\n  id?: string\n  disabled?: boolean\n  /**\n   * Id of a visible label. The trigger is then named \"label value\", for\n   * example \"Language English\", instead of the label alone.\n   */\n  labelledBy?: string\n  /** Accessible name when there is no visible label. The value is appended. */\n  \"aria-label\"?: string\n}\n\nexport type PickerSheetProps = PickerBase &\n  (\n    | {\n        multiple?: false\n        value?: string\n        onValueChange?: (value: string) => void\n      }\n    | {\n        multiple: true\n        value?: string[]\n        onValueChange?: (value: string[]) => void\n      }\n  )\n\n/**\n * A select for touch screens: a button that shows the choice and opens a\n * bottom sheet with a large-target list. The list is a WAI-ARIA listbox with\n * arrow keys, Home, End and type-ahead through the search field. Choosing\n * closes the sheet (single) or toggles an item with a Done button (multiple).\n */\nexport function PickerSheet(props: PickerSheetProps) {\n  const {\n    options,\n    title,\n    placeholder = \"Select\",\n    searchable,\n    className,\n    disabled,\n    labelledBy,\n  } = props\n  const generated = React.useId()\n  const id = props.id ?? generated\n  const [open, setOpen] = React.useState(false)\n  const [query, setQuery] = React.useState(\"\")\n  const listRef = React.useRef<HTMLUListElement>(null)\n\n  const selected: string[] = props.multiple\n    ? (props.value ?? [])\n    : props.value\n      ? [props.value]\n      : []\n\n  const shown = options.filter((o) =>\n    o.label.toLowerCase().includes(query.trim().toLowerCase())\n  )\n\n  const choose = (option: PickerOption) => {\n    if (option.disabled) return\n    if (props.multiple) {\n      const next = selected.includes(option.value)\n        ? selected.filter((v) => v !== option.value)\n        : [...selected, option.value]\n      props.onValueChange?.(next)\n    } else {\n      props.onValueChange?.(option.value)\n      setOpen(false)\n    }\n  }\n\n  const summary = selected.length\n    ? options\n        .filter((o) => selected.includes(o.value))\n        .map((o) => o.label)\n        .join(\", \")\n    : placeholder\n\n  const move = (event: React.KeyboardEvent) => {\n    const items = Array.from(\n      listRef.current?.querySelectorAll<HTMLElement>(\n        '[role=\"option\"]:not([aria-disabled=\"true\"])'\n      ) ?? []\n    )\n    if (!items.length) return\n    const at = items.indexOf(document.activeElement as HTMLElement)\n    let next = -1\n    if (event.key === \"ArrowDown\") next = Math.min(items.length - 1, at + 1)\n    if (event.key === \"ArrowUp\") next = Math.max(0, at - 1)\n    if (event.key === \"Home\") next = 0\n    if (event.key === \"End\") next = items.length - 1\n    if (next < 0) return\n    event.preventDefault()\n    items[next].focus()\n  }\n\n  return (\n    <BottomSheet\n      open={open}\n      onOpenChange={(next) => {\n        setOpen(next)\n        if (!next) setQuery(\"\")\n      }}\n      snapPoints={searchable ? [0.6, 0.92] : undefined}\n    >\n      <BottomSheetTrigger\n        id={id}\n        disabled={disabled}\n        aria-labelledby={labelledBy ? `${labelledBy} ${id}` : undefined}\n        aria-label={\n          !labelledBy && props[\"aria-label\"]\n            ? `${props[\"aria-label\"]}: ${summary}`\n            : undefined\n        }\n        className={cn(\n          \"border-input bg-background focus-visible:ring-ring flex h-11 w-full items-center justify-between gap-2 rounded-lg border px-3 text-start text-base outline-none focus-visible:ring-2 disabled:opacity-50\",\n          className\n        )}\n      >\n        <span\n          className={cn(\n            \"truncate\",\n            !selected.length && \"text-muted-foreground\"\n          )}\n        >\n          {summary}\n        </span>\n        <ChevronDownIcon\n          aria-hidden\n          className=\"text-muted-foreground size-4 shrink-0\"\n        />\n      </BottomSheetTrigger>\n      <BottomSheetContent\n        onOpenAutoFocus={(event) => {\n          // Land on the selected option, or the first one.\n          event.preventDefault()\n          const list = listRef.current\n          const target =\n            list?.querySelector<HTMLElement>('[aria-selected=\"true\"]') ??\n            list?.querySelector<HTMLElement>('[role=\"option\"]')\n          ;(searchable\n            ? list?.parentElement?.querySelector(\"input\")\n            : target\n          )?.focus()\n        }}\n      >\n        <BottomSheetHeader>\n          <BottomSheetTitle>{title}</BottomSheetTitle>\n          <BottomSheetDescription className=\"sr-only\">\n            {props.multiple\n              ? \"Choose one or more options.\"\n              : \"Choose one option.\"}\n          </BottomSheetDescription>\n        </BottomSheetHeader>\n        {searchable && (\n          <div className=\"relative px-4 pb-2\">\n            <SearchIcon\n              aria-hidden\n              className=\"text-muted-foreground pointer-events-none absolute start-7 top-3 size-4\"\n            />\n            <input\n              type=\"search\"\n              aria-label={`Search ${title}`}\n              value={query}\n              onChange={(event) => setQuery(event.target.value)}\n              onKeyDown={(event) => {\n                if (event.key === \"ArrowDown\") move(event)\n              }}\n              placeholder=\"Search\"\n              className=\"bg-muted focus-visible:ring-ring h-10 w-full rounded-lg ps-9 pe-3 text-base outline-none focus-visible:ring-2\"\n            />\n          </div>\n        )}\n        <ul\n          ref={listRef}\n          role=\"listbox\"\n          aria-label={title}\n          aria-multiselectable={props.multiple || undefined}\n          onKeyDown={move}\n          className=\"flex-1 overflow-y-auto px-2 pb-2\"\n        >\n          {shown.map((option) => {\n            const isSelected = selected.includes(option.value)\n            return (\n              <li\n                key={option.value}\n                role=\"option\"\n                aria-selected={isSelected}\n                aria-disabled={option.disabled || undefined}\n                tabIndex={option.disabled ? undefined : -1}\n                onClick={() => choose(option)}\n                onKeyDown={(event) => {\n                  if (event.key === \"Enter\" || event.key === \" \") {\n                    event.preventDefault()\n                    choose(option)\n                  }\n                }}\n                className={cn(\n                  \"focus-visible:bg-accent hover:bg-accent flex min-h-12 cursor-pointer items-center gap-3 rounded-lg px-3 py-2 outline-none aria-disabled:cursor-not-allowed aria-disabled:opacity-50 [&_svg]:size-5\",\n                  isSelected && \"font-medium\"\n                )}\n              >\n                {option.icon}\n                <span className=\"flex min-w-0 flex-1 flex-col\">\n                  <span className=\"truncate text-base\">{option.label}</span>\n                  {option.description && (\n                    <span className=\"text-muted-foreground truncate text-sm\">\n                      {option.description}\n                    </span>\n                  )}\n                </span>\n                {isSelected && (\n                  <CheckIcon aria-hidden className=\"text-primary\" />\n                )}\n              </li>\n            )\n          })}\n          {shown.length === 0 && (\n            <li\n              role=\"presentation\"\n              className=\"text-muted-foreground px-3 py-6 text-center text-sm\"\n            >\n              No results\n            </li>\n          )}\n        </ul>\n        {props.multiple && (\n          <div className=\"border-t p-4\">\n            <BottomSheetClose className=\"bg-primary text-primary-foreground focus-visible:ring-ring h-11 w-full rounded-lg font-medium outline-none focus-visible:ring-2\">\n              Done{selected.length ? ` (${selected.length})` : \"\"}\n            </BottomSheetClose>\n          </div>\n        )}\n      </BottomSheetContent>\n    </BottomSheet>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}