{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "search-view",
  "title": "Search View",
  "description": "The full-screen search surface a search bar expands into.",
  "registryDependencies": [
    "https://guillermo-rebolledo.github.io/materialcn/r/button.json",
    "https://guillermo-rebolledo.github.io/materialcn/r/list.json",
    "https://guillermo-rebolledo.github.io/materialcn/r/materialcn-theme.json",
    "https://guillermo-rebolledo.github.io/materialcn/r/search-bar.json",
    "https://guillermo-rebolledo.github.io/materialcn/r/utils.json"
  ],
  "files": [
    {
      "path": "src/components/ui/search-view.tsx",
      "content": "import {\n  useCallback,\n  useEffect,\n  useRef,\n  useContext,\n  type ComponentProps,\n  type KeyboardEvent,\n  type MouseEvent,\n} from \"react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Button } from \"./button\"\nimport { List, ListItem } from \"./list\"\nimport { SearchBar, type SearchBarProps } from \"./search-bar\"\nimport {\n  SearchViewContentContext,\n  SearchViewContext,\n  useSearchView,\n  type SearchViewState,\n} from \"./search-view-context\"\n\ntype SearchViewPresentation = \"docked\" | \"full-screen\"\n\ntype SearchViewProps = Omit<\n  ComponentProps<\"section\">,\n  \"defaultValue\" | \"onSelect\"\n> & {\n  onOpenChange: (open: boolean) => void\n  onSelect?: (value: string) => void\n  onValueChange: (value: string) => void\n  open: boolean\n  presentation?: SearchViewPresentation\n  value: string\n}\n\nfunction assignRef<T>(ref: React.Ref<T> | undefined, value: T | null) {\n  if (typeof ref === \"function\") ref(value)\n  else if (ref) ref.current = value\n}\n\nfunction SearchView({\n  \"aria-label\": ariaLabel = \"Search\",\n  children,\n  className,\n  onKeyDown,\n  onOpenChange,\n  onSelect,\n  onValueChange,\n  open,\n  presentation = \"docked\",\n  ref,\n  value,\n  ...props\n}: SearchViewProps) {\n  const rootRef = useRef<HTMLElement>(null)\n  const returnFocusRef = useRef<HTMLElement | null>(null)\n  const wasOpenRef = useRef(false)\n\n  const close = useCallback(() => onOpenChange(false), [onOpenChange])\n\n  useEffect(() => {\n    let frame = 0\n    if (open && !wasOpenRef.current) {\n      returnFocusRef.current =\n        document.activeElement instanceof HTMLElement\n          ? document.activeElement\n          : null\n      frame = requestAnimationFrame(() => {\n        rootRef.current\n          ?.querySelector<HTMLInputElement>('[data-slot=\"input-group-control\"]')\n          ?.focus()\n      })\n    } else if (!open && wasOpenRef.current) {\n      returnFocusRef.current?.focus()\n    }\n    wasOpenRef.current = open\n    return () => cancelAnimationFrame(frame)\n  }, [open])\n\n  const handleKeyDown = (event: KeyboardEvent<HTMLElement>) => {\n    onKeyDown?.(event)\n    if (event.defaultPrevented) return\n\n    if (event.key === \"Escape\") {\n      event.preventDefault()\n      close()\n      return\n    }\n\n    if (event.key !== \"ArrowDown\" && event.key !== \"ArrowUp\") return\n    const items = Array.from(\n      rootRef.current?.querySelectorAll<HTMLButtonElement>(\n        '[data-slot=\"search-view-item\"]:not(:disabled):not([aria-disabled=\"true\"])',\n      ) ?? [],\n    )\n    if (!items.length) return\n\n    const currentIndex = items.indexOf(document.activeElement as HTMLButtonElement)\n    const inputFocused =\n      document.activeElement?.getAttribute(\"data-slot\") ===\n      \"input-group-control\"\n    if (!inputFocused && currentIndex < 0) return\n\n    event.preventDefault()\n    const nextIndex = inputFocused\n      ? event.key === \"ArrowDown\"\n        ? 0\n        : items.length - 1\n      : event.key === \"ArrowDown\"\n        ? (currentIndex + 1) % items.length\n        : (currentIndex - 1 + items.length) % items.length\n    items[nextIndex]?.focus()\n  }\n\n  if (!open) return null\n\n  return (\n    <SearchViewContext.Provider\n      value={{ close, onSelect, setValue: onValueChange, value }}\n    >\n      <section\n        {...props}\n        ref={(node) => {\n          rootRef.current = node\n          assignRef(ref, node)\n        }}\n        aria-label={ariaLabel}\n        data-presentation={presentation}\n        data-slot=\"search-view\"\n        className={cn(\n          // Docked: the kit keeps the 28dp search bar and a separate\n          // 12dp-radius Surface Container High list 2dp beneath it, unelevated.\n          \"flex max-w-full flex-col text-foreground\",\n          \"data-[presentation=docked]:w-[360px] data-[presentation=docked]:gap-0.5\",\n          \"data-[presentation=full-screen]:h-full data-[presentation=full-screen]:w-full data-[presentation=full-screen]:overflow-hidden data-[presentation=full-screen]:bg-m3-surface-container-high\",\n          \"motion-reduce:transition-none\",\n          className,\n        )}\n        onKeyDown={handleKeyDown}\n      >\n        {children}\n      </section>\n    </SearchViewContext.Provider>\n  )\n}\n\nfunction SearchViewBar({ className, ...props }: Omit<SearchBarProps, \"value\" | \"defaultValue\" | \"onValueChange\">) {\n  const context = useSearchView()\n  return (\n    <SearchBar\n      {...props}\n      value={context.value}\n      onValueChange={context.setValue}\n      className={cn(\n        \"shrink-0\",\n        \"in-data-[presentation=full-screen]:[&_[data-slot=input-group]]:rounded-none\",\n        className,\n      )}\n    />\n  )\n}\n\nfunction SearchViewContent({\n  className,\n  state = \"results\",\n  ...props\n}: ComponentProps<\"div\"> & { state?: SearchViewState }) {\n  return (\n    <SearchViewContentContext.Provider value={state}>\n      <div\n        {...props}\n        data-slot=\"search-view-content\"\n        data-state={state}\n        className={cn(\n          \"min-h-0 flex-1 overflow-y-auto overscroll-contain\",\n          \"in-data-[presentation=docked]:max-h-48 in-data-[presentation=docked]:rounded-m3-md in-data-[presentation=docked]:bg-m3-surface-container-high in-data-[presentation=docked]:py-1\",\n          className,\n        )}\n      />\n    </SearchViewContentContext.Provider>\n  )\n}\n\nfunction SearchViewList({ className, ...props }: ComponentProps<typeof List>) {\n  return (\n    <List\n      {...props}\n      role=\"listbox\"\n      data-slot=\"search-view-list\"\n      className={cn(\"py-1\", className)}\n    />\n  )\n}\n\ntype SearchViewItemProps = Omit<ComponentProps<\"button\">, \"value\"> & {\n  lines?: 1 | 2 | 3\n  value: string\n}\n\nfunction SearchViewItem({\n  children,\n  disabled,\n  lines = 1,\n  onClick,\n  value,\n  ...props\n}: SearchViewItemProps) {\n  const context = useSearchView()\n  const ariaDisabled =\n    props[\"aria-disabled\"] === true || props[\"aria-disabled\"] === \"true\"\n  return (\n    <ListItem\n      lines={lines}\n      wrapperRole=\"presentation\"\n      render={\n        <button\n          {...props}\n          type=\"button\"\n          role=\"option\"\n          tabIndex={-1}\n          data-slot=\"search-view-item\"\n          data-value={value}\n          disabled={disabled}\n          aria-selected={false}\n          onClick={(event: MouseEvent<HTMLButtonElement>) => {\n            if (disabled || ariaDisabled) return\n            onClick?.(event)\n            if (event.defaultPrevented) return\n            context.onSelect?.(value)\n            context.close()\n          }}\n        />\n      }\n    >\n      {children}\n    </ListItem>\n  )\n}\n\nfunction SearchViewClose({\n  \"aria-label\": ariaLabel = \"Close search\",\n  onClick,\n  ...props\n}: ComponentProps<typeof Button>) {\n  const context = useSearchView()\n  return (\n    <Button\n      {...props}\n      type=\"button\"\n      aria-label={ariaLabel}\n      data-slot=\"search-view-close\"\n      size=\"icon\"\n      variant=\"ghost\"\n      onClick={(event) => {\n        onClick?.(event)\n        if (!event.defaultPrevented) context.close()\n      }}\n    />\n  )\n}\n\nfunction SearchViewMessage({\n  className,\n  role,\n  ...props\n}: ComponentProps<\"div\">) {\n  const state = useContext(SearchViewContentContext)\n  return (\n    <div\n      {...props}\n      role={role ?? (state === \"error\" ? \"alert\" : \"status\")}\n      data-slot=\"search-view-message\"\n      className={cn(\n        \"flex min-h-32 items-center justify-center px-6 text-center text-m3-body-md text-muted-foreground\",\n        state === \"error\" && \"text-m3-error\",\n        className,\n      )}\n    />\n  )\n}\n\nexport {\n  SearchView,\n  SearchViewBar,\n  SearchViewClose,\n  SearchViewContent,\n  SearchViewItem,\n  SearchViewList,\n  SearchViewMessage,\n  type SearchViewPresentation,\n  type SearchViewProps,\n  type SearchViewState,\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "src/components/ui/search-view-context.ts",
      "content": "import { createContext, useContext } from \"react\"\n\ntype SearchViewContextValue = {\n  close: () => void\n  onSelect?: (value: string) => void\n  setValue: (value: string) => void\n  value: string\n}\n\nconst SearchViewContext = createContext<SearchViewContextValue | null>(null)\n\nfunction useSearchView() {\n  const context = useContext(SearchViewContext)\n  if (!context) {\n    throw new Error(\"SearchView components must be used within SearchView\")\n  }\n  return context\n}\n\ntype SearchViewState =\n  | \"recent\"\n  | \"suggestions\"\n  | \"loading\"\n  | \"results\"\n  | \"empty\"\n  | \"error\"\n\nconst SearchViewContentContext = createContext<SearchViewState>(\"results\")\n\nexport {\n  SearchViewContentContext,\n  SearchViewContext,\n  useSearchView,\n  type SearchViewState,\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "forms"
  ],
  "type": "registry:ui"
}