{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "p-autocomplete-16",
  "description": "Address autocomplete with Google Maps Places API",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "@coss/autocomplete",
    "@coss/spinner"
  ],
  "files": [
    {
      "path": "registry/default/particles/p-autocomplete-16.tsx",
      "content": "\"use client\";\n\nimport { MapPinIcon } from \"lucide-react\";\nimport type { ReactNode } from \"react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport {\n  Autocomplete,\n  AutocompleteInput,\n  AutocompleteItem,\n  AutocompleteList,\n  AutocompletePopup,\n  AutocompleteStatus,\n} from \"@/registry/default/ui/autocomplete\";\nimport { Spinner } from \"@/registry/default/ui/spinner\";\n\n// Set NEXT_PUBLIC_GOOGLE_MAPS_API_KEY with the Places API (New) enabled to fetch\n// live suggestions. Without a key, the demo falls back to sample addresses.\nconst GOOGLE_MAPS_API_KEY = process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY ?? \"\";\n\ntype AddressSuggestion = {\n  placeId: string;\n  text: string;\n  mainText: string;\n  secondaryText: string;\n};\n\nconst sampleAddresses: AddressSuggestion[] = [\n  {\n    mainText: \"1600 Amphitheatre Parkway\",\n    secondaryText: \"Mountain View, CA 94043, USA\",\n  },\n  {\n    mainText: \"1600 Pennsylvania Avenue NW\",\n    secondaryText: \"Washington, DC 20500, USA\",\n  },\n  { mainText: \"350 Fifth Avenue\", secondaryText: \"New York, NY 10118, USA\" },\n  {\n    mainText: \"221B Baker Street\",\n    secondaryText: \"London NW1 6XE, United Kingdom\",\n  },\n  {\n    mainText: \"Champ de Mars, 5 Avenue Anatole France\",\n    secondaryText: \"75007 Paris, France\",\n  },\n  {\n    mainText: \"Piazza del Colosseo, 1\",\n    secondaryText: \"00184 Roma RM, Italy\",\n  },\n  { mainText: \"Platz der Republik 1\", secondaryText: \"11011 Berlin, Germany\" },\n  {\n    mainText: \"1 Macquarie Street\",\n    secondaryText: \"Sydney NSW 2000, Australia\",\n  },\n].map((address, index) => ({\n  ...address,\n  placeId: `sample-${index + 1}`,\n  text: `${address.mainText}, ${address.secondaryText}`,\n}));\n\n// A short-lived session token groups Autocomplete + Place Details requests\n// for billing.\nfunction newSessionToken() {\n  if (typeof crypto !== \"undefined\" && \"randomUUID\" in crypto) {\n    return crypto.randomUUID();\n  }\n  return Math.random().toString(36).slice(2);\n}\n\ntype PlacesAutocompleteResponse = {\n  suggestions?: {\n    placePrediction?: {\n      placeId?: string;\n      text?: { text?: string };\n      structuredFormat?: {\n        mainText?: { text?: string };\n        secondaryText?: { text?: string };\n      };\n    };\n  }[];\n};\n\nasync function fetchAddressSuggestions(\n  query: string,\n  sessionToken: string,\n  signal: AbortSignal,\n): Promise<AddressSuggestion[]> {\n  const response = await fetch(\n    \"https://places.googleapis.com/v1/places:autocomplete\",\n    {\n      body: JSON.stringify({ input: query, sessionToken }),\n      headers: {\n        \"Content-Type\": \"application/json\",\n        \"X-Goog-Api-Key\": GOOGLE_MAPS_API_KEY,\n      },\n      method: \"POST\",\n      signal,\n    },\n  );\n  if (!response.ok) {\n    throw new Error(\"Places API request failed\");\n  }\n  const data = (await response.json()) as PlacesAutocompleteResponse;\n  const suggestions: AddressSuggestion[] = [];\n  for (const suggestion of data.suggestions ?? []) {\n    const prediction = suggestion.placePrediction;\n    if (!prediction?.placeId) continue;\n    const text = prediction.text?.text ?? \"\";\n    suggestions.push({\n      mainText: prediction.structuredFormat?.mainText?.text ?? text,\n      placeId: prediction.placeId,\n      secondaryText: prediction.structuredFormat?.secondaryText?.text ?? \"\",\n      text,\n    });\n  }\n  return suggestions;\n}\n\nasync function searchSampleAddresses(\n  query: string,\n): Promise<AddressSuggestion[]> {\n  await new Promise((resolve) =>\n    setTimeout(resolve, Math.random() * 500 + 100),\n  );\n  const lowerQuery = query.toLowerCase();\n  return sampleAddresses.filter((address) =>\n    address.text.toLowerCase().includes(lowerQuery),\n  );\n}\n\nexport default function Particle() {\n  const [searchValue, setSearchValue] = useState(\"\");\n  const [isLoading, setIsLoading] = useState(false);\n  const [suggestions, setSuggestions] = useState<AddressSuggestion[]>([]);\n  const [error, setError] = useState<string | null>(null);\n  const sessionTokenRef = useRef<string | null>(null);\n\n  useEffect(() => {\n    const query = searchValue.trim();\n    if (!query) {\n      setSuggestions([]);\n      setIsLoading(false);\n      setError(null);\n      return;\n    }\n\n    setIsLoading(true);\n    setError(null);\n    let ignore = false;\n    const controller = new AbortController();\n\n    const timeoutId = setTimeout(async () => {\n      try {\n        sessionTokenRef.current ??= newSessionToken();\n        const results = GOOGLE_MAPS_API_KEY\n          ? await fetchAddressSuggestions(\n              query,\n              sessionTokenRef.current,\n              controller.signal,\n            )\n          : await searchSampleAddresses(query);\n        if (!ignore) setSuggestions(results);\n      } catch {\n        if (!ignore && !controller.signal.aborted) {\n          setError(\"Could not load address suggestions. Please try again.\");\n          setSuggestions([]);\n        }\n      } finally {\n        if (!ignore) setIsLoading(false);\n      }\n    }, 300);\n\n    return () => {\n      ignore = true;\n      controller.abort();\n      clearTimeout(timeoutId);\n    };\n  }, [searchValue]);\n\n  let status: ReactNode = `${suggestions.length} suggestion${suggestions.length === 1 ? \"\" : \"s\"} found`;\n  if (isLoading) {\n    status = (\n      <span className=\"flex items-center justify-between gap-2 text-muted-foreground\">\n        Searching addresses...\n        <Spinner className=\"size-4.5 sm:size-4\" />\n      </span>\n    );\n  } else if (error) {\n    status = (\n      <span className=\"font-normal text-destructive text-sm\">{error}</span>\n    );\n  } else if (suggestions.length === 0 && searchValue) {\n    status = (\n      <span className=\"font-normal text-muted-foreground text-sm\">\n        No addresses found for \"{searchValue}\"\n      </span>\n    );\n  }\n\n  const shouldRenderPopup = searchValue.trim() !== \"\";\n\n  return (\n    <Autocomplete\n      autoHighlight\n      filter={null}\n      items={suggestions}\n      itemToStringValue={(item: unknown) => (item as AddressSuggestion).text}\n      onValueChange={(value, eventDetails) => {\n        setSearchValue(value);\n        if (eventDetails.reason === \"item-press\") {\n          // Selecting a suggestion ends the billing session. In a real app,\n          // fetch the place details with the same token before resetting it.\n          sessionTokenRef.current = null;\n        }\n      }}\n      value={searchValue}\n    >\n      <AutocompleteInput\n        aria-label=\"Address\"\n        autoComplete=\"off\"\n        className=\"min-w-0 *:[input]:truncate\"\n        placeholder=\"Enter an address\"\n        startAddon={<MapPinIcon />}\n      />\n      {shouldRenderPopup && (\n        <AutocompletePopup\n          aria-busy={isLoading || undefined}\n          className=\"max-w-(--anchor-width) *:min-w-0\"\n        >\n          <AutocompleteStatus className=\"text-muted-foreground\">\n            {status}\n          </AutocompleteStatus>\n          <AutocompleteList>\n            {(suggestion: AddressSuggestion) => (\n              <AutocompleteItem key={suggestion.placeId} value={suggestion}>\n                <span className=\"flex w-full min-w-0 flex-col\">\n                  <span className=\"truncate font-medium\">\n                    {suggestion.mainText}\n                  </span>\n                  <span className=\"truncate text-muted-foreground text-xs\">\n                    {suggestion.secondaryText}\n                  </span>\n                </span>\n              </AutocompleteItem>\n            )}\n          </AutocompleteList>\n        </AutocompletePopup>\n      )}\n    </Autocomplete>\n  );\n}\n",
      "type": "registry:block"
    }
  ],
  "meta": {
    "className": "**:data-[slot=preview]:w-full **:data-[slot=preview]:max-w-64"
  },
  "categories": [
    "autocomplete",
    "input",
    "async",
    "search"
  ],
  "type": "registry:block"
}