{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "infinite-query-hook",
  "title": "Infinite Query Hook",
  "description": "React hook for infinite lists, fetching data from Supabase.",
  "dependencies": [
    "@supabase/supabase-js",
    "@supabase/postgrest-js@*"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/blocks/infinite-query-hook/hooks/use-infinite-query.ts",
      "content": "'use client'\n\nimport { PostgrestQueryBuilder, type PostgrestClientOptions } from '@supabase/postgrest-js'\nimport { type SupabaseClient } from '@supabase/supabase-js'\nimport { useEffect, useMemo, useRef, useSyncExternalStore } from 'react'\n\nimport { createClient } from '@/registry/default/fixtures/lib/supabase/client'\n\nconst supabase = createClient()\n\n// The following types are used to make the hook type-safe. It extracts the database type from the supabase client.\ntype SupabaseClientType = typeof supabase\n\n// Utility type to check if the type is any\ntype IfAny<T, Y, N> = 0 extends 1 & T ? Y : N\n\n// Extracts the database type from the supabase client. If the supabase client doesn't have a type, it will fallback properly.\ntype Database =\n  SupabaseClientType extends SupabaseClient<infer U>\n    ? IfAny<\n        U,\n        {\n          public: {\n            Tables: Record<string, any>\n            Views: Record<string, any>\n            Functions: Record<string, any>\n          }\n        },\n        U\n      >\n    : {\n        public: {\n          Tables: Record<string, any>\n          Views: Record<string, any>\n          Functions: Record<string, any>\n        }\n      }\n\n// Change this to the database schema you want to use\ntype DatabaseSchema = Database['public']\n\n// Extracts the table names from the database type\ntype SupabaseTableName = keyof DatabaseSchema['Tables']\n\n// Extracts the table definition from the database type\ntype SupabaseTableData<T extends SupabaseTableName> = DatabaseSchema['Tables'][T]['Row']\n\n// Default client options for PostgrestQueryBuilder\ntype DefaultClientOptions = PostgrestClientOptions\n\ntype SupabaseSelectBuilder<T extends SupabaseTableName> = ReturnType<\n  PostgrestQueryBuilder<\n    DefaultClientOptions,\n    DatabaseSchema,\n    DatabaseSchema['Tables'][T],\n    T\n  >['select']\n>\n\n// A function that modifies the query. Can be used to sort, filter, etc. If .range is used, it will be overwritten.\ntype SupabaseQueryHandler<T extends SupabaseTableName> = (\n  query: SupabaseSelectBuilder<T>\n) => SupabaseSelectBuilder<T>\n\ninterface UseInfiniteQueryProps<T extends SupabaseTableName, Query extends string = '*'> {\n  // The table name to query\n  tableName: T\n  // The columns to select, defaults to `*`\n  columns?: string\n  // The number of items to fetch per page, defaults to `20`\n  pageSize?: number\n  // A function that modifies the query. Can be used to sort, filter, etc. If .range is used, it will be overwritten.\n  trailingQuery?: SupabaseQueryHandler<T>\n  // Optional key that identifies the current trailing query shape (e.g. filters/sort/search).\n  // When this changes, the internal store is recreated so stale paginated rows are discarded.\n  trailingQueryKey?: unknown\n}\n\ninterface StoreState<TData> {\n  data: TData[]\n  count: number\n  isSuccess: boolean\n  isLoading: boolean\n  isFetching: boolean\n  error: Error | null\n  hasInitialFetch: boolean\n}\n\ntype Listener = () => void\n\ninterface StoreProps<T extends SupabaseTableName> {\n  tableName: T\n  columns?: string\n  pageSize?: number\n  getTrailingQuery: () => SupabaseQueryHandler<T> | undefined\n}\n\nfunction createStore<TData extends SupabaseTableData<T>, T extends SupabaseTableName>(\n  props: StoreProps<T>\n) {\n  const { tableName, columns = '*', pageSize = 20, getTrailingQuery } = props\n\n  let state: StoreState<TData> = {\n    data: [],\n    count: 0,\n    isSuccess: false,\n    isLoading: false,\n    isFetching: false,\n    error: null,\n    hasInitialFetch: false,\n  }\n\n  const listeners = new Set<Listener>()\n\n  const notify = () => {\n    listeners.forEach((listener) => listener())\n  }\n\n  const setState = (newState: Partial<StoreState<TData>>) => {\n    state = { ...state, ...newState }\n    notify()\n  }\n\n  const fetchPage = async (skip: number) => {\n    if (state.hasInitialFetch && (state.isFetching || state.count <= state.data.length)) return\n\n    setState({ isFetching: true })\n\n    let query = supabase\n      .from(tableName)\n      .select(columns, { count: 'exact' }) as unknown as SupabaseSelectBuilder<T>\n\n    const trailingQuery = getTrailingQuery()\n    if (trailingQuery) {\n      query = trailingQuery(query)\n    }\n    const { data: newData, count, error } = await query.range(skip, skip + pageSize - 1)\n\n    if (error) {\n      console.error('An unexpected error occurred:', error)\n      setState({ error })\n    } else {\n      setState({\n        data: [...state.data, ...(newData as TData[])],\n        count: count || 0,\n        isSuccess: true,\n        error: null,\n      })\n    }\n    setState({ isFetching: false })\n  }\n\n  const fetchNextPage = async () => {\n    if (state.isFetching) return\n    await fetchPage(state.data.length)\n  }\n\n  const initialize = async () => {\n    setState({ isLoading: true, isSuccess: false, data: [] })\n    await fetchNextPage()\n    setState({ isLoading: false, hasInitialFetch: true })\n  }\n\n  return {\n    getState: () => state,\n    subscribe: (listener: Listener) => {\n      listeners.add(listener)\n      return () => listeners.delete(listener)\n    },\n    fetchNextPage,\n    initialize,\n  }\n}\n\n// Empty initial state to avoid hydration errors.\nconst initialState: any = {\n  data: [],\n  count: 0,\n  isSuccess: false,\n  isLoading: false,\n  isFetching: false,\n  error: null,\n  hasInitialFetch: false,\n}\n\nfunction useInfiniteQuery<\n  TData extends SupabaseTableData<T>,\n  T extends SupabaseTableName = SupabaseTableName,\n>(props: UseInfiniteQueryProps<T>) {\n  const tableName = props.tableName\n  const columns = props.columns ?? '*'\n  const pageSize = props.pageSize ?? 20\n  const trailingQuery = props.trailingQuery\n  const trailingQueryKey = props.trailingQueryKey\n  const trailingQueryRef = useRef(trailingQuery)\n\n  trailingQueryRef.current = trailingQuery\n\n  const store = useMemo(\n    () =>\n      createStore<TData, T>({\n        tableName,\n        columns,\n        pageSize,\n        getTrailingQuery: () => trailingQueryRef.current,\n      }),\n    [tableName, columns, pageSize, trailingQueryKey]\n  )\n\n  const state = useSyncExternalStore(\n    store.subscribe,\n    () => store.getState(),\n    () => initialState as StoreState<TData>\n  )\n\n  useEffect(() => {\n    if (!state.hasInitialFetch && typeof window !== 'undefined') {\n      store.initialize()\n    }\n  }, [state.hasInitialFetch, store])\n\n  return {\n    data: state.data,\n    count: state.count,\n    isSuccess: state.isSuccess,\n    isLoading: state.isLoading,\n    isFetching: state.isFetching,\n    error: state.error,\n    hasMore: state.count > state.data.length,\n    fetchNextPage: store.fetchNextPage,\n  }\n}\n\nexport {\n  useInfiniteQuery,\n  type SupabaseQueryHandler,\n  type SupabaseTableData,\n  type SupabaseTableName,\n  type UseInfiniteQueryProps,\n}\n",
      "type": "registry:hook"
    }
  ],
  "type": "registry:block"
}