{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "search-bar",
  "type": "registry:component",
  "title": "Search Bar",
  "description": "Text search input with icon and clear functionality.",
  "dependencies": [
    "@vllnt/ui@^0.2.1"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/search-bar/search-bar.tsx",
      "content": "\"use client\";\n\nimport { Suspense, useEffect, useRef, useState } from \"react\";\n\nimport { useRouter, useSearchParams } from \"next/navigation\";\n\nimport { useDebounce } from \"@vllnt/ui\";\nimport { Button } from \"@vllnt/ui\";\nimport { Input } from \"@vllnt/ui\";\n\ntype SearchBarProps = {\n  className?: string;\n  onSearch?: (query: string) => void;\n  placeholder?: string;\n};\n\nexport function SearchBar(props: SearchBarProps) {\n  // useSearchParams suspends during SSR — Suspense boundary keeps the\n  // surrounding tree streamable. See react-doctor rule\n  // nextjs-no-use-search-params-without-suspense + Next.js docs.\n  return (\n    <Suspense fallback={<SearchBarFallback {...props} />}>\n      <SearchBarInner {...props} />\n    </Suspense>\n  );\n}\n\nfunction SearchBarFallback({\n  className,\n  placeholder = \"Search posts...\",\n}: SearchBarProps) {\n  return (\n    <form className={`flex gap-2 ${className}`}>\n      <Input\n        aria-label={placeholder}\n        className=\"flex-1\"\n        disabled\n        placeholder={placeholder}\n        type=\"text\"\n        value=\"\"\n      />\n      <Button disabled type=\"submit\" variant=\"outline\">\n        Search\n      </Button>\n    </form>\n  );\n}\n\nfunction SearchBarInner({\n  className,\n  onSearch,\n  placeholder = \"Search posts...\",\n}: SearchBarProps) {\n  const router = useRouter();\n  const searchParameters = useSearchParams();\n  const initialQuery = searchParameters.get(\"search\") ?? \"\";\n  const [query, setQuery] = useState(initialQuery);\n  const debouncedQuery = useDebounce(query, 300);\n  const isInitialMount = useRef(true);\n  const isUserTyping = useRef(false);\n\n  const typingTimeoutReference = useRef<NodeJS.Timeout | undefined>(undefined);\n  const lastSetSearchParameterReference = useRef<string>(\"\");\n  const lastDebouncedQueryReference = useRef<string>(\"\");\n\n  // Sync query with URL search params (e.g., on browser back/forward)\n  // Sync when user is not actively typing and URL changed externally\n  useEffect(() => {\n    const searchParameter = searchParameters.get(\"search\") ?? \"\";\n\n    // Skip if this is the search param we set ourselves\n    if (searchParameter === lastSetSearchParameterReference.current) {\n      return;\n    }\n\n    // Sync if user is not actively typing and values differ\n    if (!isUserTyping.current && query !== searchParameter) {\n      requestAnimationFrame(() => {\n        setQuery(searchParameter);\n        lastDebouncedQueryReference.current = searchParameter;\n      });\n    }\n  }, [searchParameters, query]); // Include query to properly sync state\n\n  // Update URL when debounced query changes\n  useEffect(() => {\n    // Skip initial mount to avoid unnecessary URL update\n    if (isInitialMount.current) {\n      isInitialMount.current = false;\n      const initialTrimmed = debouncedQuery.trim();\n      lastDebouncedQueryReference.current = initialTrimmed;\n      lastSetSearchParameterReference.current = initialTrimmed;\n      return;\n    }\n\n    const trimmedQuery = debouncedQuery.trim();\n\n    // Skip if this is the same value we already processed\n    if (trimmedQuery === lastDebouncedQueryReference.current) {\n      return;\n    }\n\n    lastDebouncedQueryReference.current = trimmedQuery;\n\n    if (onSearch) {\n      onSearch(trimmedQuery);\n      return;\n    }\n\n    // Check current URL to avoid unnecessary updates\n    const currentUrlParameter = searchParameters.get(\"search\") ?? \"\";\n\n    // Skip if URL already matches the debounced query\n    if (trimmedQuery === currentUrlParameter) {\n      lastSetSearchParameterReference.current = trimmedQuery;\n      return;\n    }\n\n    const parameters = new URLSearchParams(searchParameters);\n    if (trimmedQuery) {\n      parameters.set(\"search\", trimmedQuery);\n    } else {\n      parameters.delete(\"search\");\n    }\n    const newUrl = parameters.toString();\n    lastSetSearchParameterReference.current = trimmedQuery;\n    // next/navigation router.replace is the canonical client-side\n    // navigation primitive in Next App Router. The react-doctor\n    // nextjs-no-client-side-redirect rule targets window.location\n    // hard redirects, not Next router.replace — but ESLint doesn't\n    // know that rule, so we don't add an eslint-disable for it.\n    router.replace(`?${newUrl}`);\n  }, [debouncedQuery, router, onSearch, searchParameters]);\n\n  // Cleanup timeout on unmount\n  useEffect(() => {\n    return () => {\n      if (typingTimeoutReference.current) {\n        clearTimeout(typingTimeoutReference.current);\n      }\n    };\n  }, []);\n\n  const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {\n    isUserTyping.current = true;\n    setQuery(event.target.value);\n\n    // Clear existing timeout\n    if (typingTimeoutReference.current) {\n      clearTimeout(typingTimeoutReference.current);\n    }\n\n    // Reset typing flag after debounce delay + buffer\n    typingTimeoutReference.current = setTimeout(() => {\n      isUserTyping.current = false;\n    }, 350);\n  };\n\n  const handleSubmit = (event: React.SyntheticEvent) => {\n    event.preventDefault();\n    isUserTyping.current = false;\n\n    // Clear typing timeout\n    if (typingTimeoutReference.current) {\n      clearTimeout(typingTimeoutReference.current);\n    }\n\n    const trimmedQuery = query.trim();\n    if (onSearch) {\n      onSearch(trimmedQuery);\n    } else {\n      const parameters = new URLSearchParams(searchParameters);\n      if (trimmedQuery) {\n        parameters.set(\"search\", trimmedQuery);\n      } else {\n        parameters.delete(\"search\");\n      }\n      const newUrl = parameters.toString();\n      lastSetSearchParameterReference.current = trimmedQuery;\n      router.replace(`?${newUrl}`);\n    }\n  };\n\n  return (\n    <form className={`flex gap-2 ${className}`} onSubmit={handleSubmit}>\n      <Input\n        aria-label={placeholder}\n        className=\"flex-1\"\n        onChange={handleInputChange}\n        placeholder={placeholder}\n        type=\"text\"\n        value={query}\n      />\n      <Button type=\"submit\" variant=\"outline\">\n        Search\n      </Button>\n    </form>\n  );\n}\n",
      "type": "registry:component"
    }
  ],
  "version": "0.2.1",
  "stability": "stable"
}
