{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "use-product-search",
  "title": "useProductSearch",
  "description": "Query + filter state, debounced search, and infinite-scroll pagination via TanStack Query and an injected search fetcher.",
  "dependencies": [
    "@channel3/sdk",
    "@tanstack/react-query"
  ],
  "registryDependencies": [
    "https://ui.trychannel3.com/r/search.json",
    "https://ui.trychannel3.com/r/use-in-viewport.json"
  ],
  "files": [
    {
      "path": "registry/default/hooks/use-product-search.ts",
      "content": "import * as React from \"react\";\nimport { useInfiniteQuery } from \"@tanstack/react-query\";\nimport type { Product, SearchFilters } from \"@channel3/sdk/resources\";\n\nimport {\n  EMPTY_FILTERS,\n  type SearchFiltersState,\n  toSearchFilters,\n} from \"@/registry/default/lib/search\";\nimport { useInViewport } from \"@/registry/default/hooks/use-in-viewport\";\n\nexport interface SearchFetchInput {\n  query: string;\n  imageUrl?: string;\n  /** Base64-encoded image bytes (no data-URI prefix) to search by, if any. */\n  base64Image?: string;\n  filters: SearchFilters;\n  pageToken?: string;\n}\n\nexport interface SearchPage {\n  products: Product[];\n  nextPageToken?: string | null;\n}\n\n/**\n * Runs a product search. Implement on the consumer side so the API key stays on\n * your server: call `client.products.search(...)` (or `searchByImage`) and\n * return this page's products plus its `next_page_token`.\n */\nexport type SearchFetcher = (input: SearchFetchInput) => Promise<SearchPage>;\n\nexport interface ImageQuery {\n  imageUrl?: string;\n  base64Image?: string;\n  label?: string;\n}\n\nexport interface UseProductSearchOptions {\n  fetchSearch: SearchFetcher;\n  initialQuery?: string;\n  initialFilters?: SearchFiltersState;\n  debounceMs?: number;\n  /**\n   * Search automatically as the query or filters change. When `false`, results\n   * only update when {@link UseProductSearchResult.submit} is called. Defaults to `true`.\n   */\n  autoSearch?: boolean;\n  searchOnMount?: boolean;\n}\n\nexport interface UseProductSearchResult {\n  query: string;\n  setQuery: (query: string) => void;\n  filters: SearchFiltersState;\n  setFilters: (filters: SearchFiltersState) => void;\n  searchByImage: (image: ImageQuery | null) => void;\n  image: ImageQuery | null;\n  results: Product[];\n  isLoading: boolean;\n  isLoadingMore: boolean;\n  error: unknown;\n  hasMore: boolean;\n  loadMore: () => void;\n  sentinelRef: (node: Element | null) => void;\n  submit: () => void;\n  reset: () => void;\n}\n\nconst EMPTY: Product[] = [];\n\ninterface SearchCriteria {\n  query: string;\n  filters: SearchFiltersState;\n  image: ImageQuery | null;\n}\n\nfunction hasCriteria(query: string, image: ImageQuery | null): boolean {\n  return query.trim().length > 0 || image != null;\n}\n\nexport function useProductSearch({\n  fetchSearch,\n  initialQuery = \"\",\n  initialFilters = EMPTY_FILTERS,\n  debounceMs = 350,\n  autoSearch = true,\n  searchOnMount = false,\n}: UseProductSearchOptions): UseProductSearchResult {\n  const [query, setQueryState] = React.useState(initialQuery);\n  const [filters, setFiltersState] = React.useState<SearchFiltersState>(initialFilters);\n  const [image, setImage] = React.useState<ImageQuery | null>(null);\n  const [submitNonce, setSubmitNonce] = React.useState(0);\n  const [active, setActive] = React.useState<SearchCriteria | null>(null);\n\n  const skipFirst = React.useRef(!searchOnMount);\n\n  React.useEffect(() => {\n    if (skipFirst.current && submitNonce === 0) {\n      skipFirst.current = false;\n      return;\n    }\n    if (!autoSearch && submitNonce === 0) {\n      return;\n    }\n    const timer = setTimeout(() => {\n      setActive({ query, filters, image });\n    }, debounceMs);\n    return () => clearTimeout(timer);\n  }, [query, filters, image, submitNonce, autoSearch, debounceMs]);\n\n  const enabled = active != null && hasCriteria(active.query, active.image);\n\n  const infinite = useInfiniteQuery({\n    queryKey: [\n      \"channel3-product-search\",\n      active?.query ?? \"\",\n      active ? toSearchFilters(active.filters) : {},\n      active?.image?.imageUrl ?? null,\n      active?.image?.base64Image ?? null,\n    ],\n    queryFn: ({ pageParam }) =>\n      fetchSearch({\n        query: active!.query,\n        imageUrl: active!.image?.imageUrl,\n        base64Image: active!.image?.base64Image,\n        filters: toSearchFilters(active!.filters),\n        pageToken: pageParam,\n      }),\n    initialPageParam: undefined as string | undefined,\n    getNextPageParam: (lastPage) => lastPage.nextPageToken ?? undefined,\n    enabled,\n  });\n\n  const setQuery = React.useCallback((next: string) => setQueryState(next), []);\n  const setFilters = React.useCallback((next: SearchFiltersState) => setFiltersState(next), []);\n  const searchByImage = React.useCallback((next: ImageQuery | null) => setImage(next), []);\n  const submit = React.useCallback(() => setSubmitNonce((value) => value + 1), []);\n\n  const reset = React.useCallback(() => {\n    setQueryState(\"\");\n    setFiltersState(EMPTY_FILTERS);\n    setImage(null);\n    setSubmitNonce(0);\n    setActive(null);\n  }, []);\n\n  const loadMore = () => {\n    if (!infinite.hasNextPage || infinite.isFetchingNextPage) {\n      return;\n    }\n    void infinite.fetchNextPage();\n  };\n\n  const [sentinel, setSentinel] = React.useState<Element | null>(null);\n  const sentinelRef = React.useCallback((node: Element | null) => setSentinel(node), []);\n  useInViewport(sentinel, loadMore, { enabled: Boolean(infinite.hasNextPage), rootMargin: \"200px\" });\n\n  return {\n    query,\n    setQuery,\n    filters,\n    setFilters,\n    searchByImage,\n    image,\n    results: enabled ? (infinite.data?.pages.flatMap((page) => page.products) ?? EMPTY) : EMPTY,\n    isLoading: infinite.isFetching && !infinite.isFetchingNextPage,\n    isLoadingMore: infinite.isFetchingNextPage,\n    error: infinite.error,\n    hasMore: Boolean(infinite.hasNextPage),\n    loadMore,\n    sentinelRef,\n    submit,\n    reset,\n  };\n}\n",
      "type": "registry:hook"
    }
  ],
  "type": "registry:hook"
}
