{
  "name": "rhf-address-field",
  "type": "registry:ui",
  "dependencies": [
    "@hookform/lenses",
    "lucide-react",
    "react",
    "react-hook-form",
    "zod"
  ],
  "registryDependencies": [
    "command",
    "field",
    "input",
    "popover"
  ],
  "files": [
    {
      "path": "./items/react-hook-form/address-field/component.tsx",
      "type": "registry:ui",
      "target": "./components/ui/shuip/react-hook-form/address-field.tsx",
      "content": "'use client';\n\nimport type { Lens } from '@hookform/lenses';\nimport { Loader2, MapPin } from 'lucide-react';\nimport * as React from 'react';\nimport { useController } from 'react-hook-form';\nimport { z } from 'zod';\nimport { getPlaceDetails, getPlacesAutocomplete } from '@/actions/shuip/places';\nimport { Command, CommandEmpty, CommandGroup, CommandItem, CommandList } from '@/components/ui/command';\nimport { Field, FieldError, FieldLabel } from '@/components/ui/field';\nimport { Input } from '@/components/ui/input';\nimport { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';\nimport { cn } from '@/lib/utils';\n\nconst DEFAULT_COUNTRY = 'US';\nconst LANGUAGE_RESULT = 'en';\nconst DEBOUNCE_TIME = 300;\n\nexport const addressSchema = z.object({\n  street: z.string().min(1, 'Street is required'),\n  city: z.string().min(1, 'City is required'),\n  postalCode: z.string().min(1, 'Postal code is required'),\n  country: z.string().min(1, 'Country is required'),\n  fullAddress: z.string().min(1, 'Address is required'),\n  placeId: z.string().optional(),\n});\n\nexport type AddressData = z.infer<typeof addressSchema>;\n\ninterface AddressSuggestion {\n  placeId: string;\n  description: string;\n  mainText: string;\n  secondaryText: string;\n  types: string[];\n}\n\nexport interface AddressFieldProps extends Omit<React.ComponentProps<typeof Input>, 'value' | 'onChange'> {\n  lens: Lens<AddressData>;\n  label?: string;\n  placeholder?: string;\n  description?: string;\n  country?: string;\n}\n\nexport function AddressField({\n  lens,\n  label = 'Address',\n  placeholder = 'Enter your address',\n  description,\n  country = DEFAULT_COUNTRY,\n  ...props\n}: AddressFieldProps) {\n  const fullAddress = useController(lens.focus('fullAddress').interop());\n  const street = useController(lens.focus('street').interop());\n  const city = useController(lens.focus('city').interop());\n  const postalCode = useController(lens.focus('postalCode').interop());\n  const countryField = useController(lens.focus('country').interop());\n  const placeId = useController(lens.focus('placeId').interop());\n\n  const [searchQuery, setSearchQuery] = React.useState('');\n  const [suggestions, setSuggestions] = React.useState<AddressSuggestion[]>([]);\n  const [showSuggestions, setShowSuggestions] = React.useState(false);\n  const [selectedIndex, setSelectedIndex] = React.useState(-1);\n  const [isPending, startTransition] = React.useTransition();\n  const debounceTimerRef = React.useRef<NodeJS.Timeout | null>(null);\n  const requestIdRef = React.useRef(0);\n  const inputRef = React.useRef<HTMLInputElement>(null);\n  const popoverRef = React.useRef<HTMLDivElement>(null);\n\n  React.useEffect(() => {\n    if (debounceTimerRef.current) {\n      clearTimeout(debounceTimerRef.current);\n    }\n\n    if (searchQuery.length < 3) {\n      setSuggestions([]);\n      setShowSuggestions(false);\n      return;\n    }\n\n    debounceTimerRef.current = setTimeout(() => {\n      const requestId = ++requestIdRef.current;\n      startTransition(async () => {\n        try {\n          const result = await getPlacesAutocomplete({\n            input: searchQuery,\n            components: country ? `country:${country}` : undefined,\n            types: 'address',\n            language: LANGUAGE_RESULT,\n          });\n\n          if (requestId !== requestIdRef.current) return;\n\n          if (result.error) {\n            throw new Error(result.error);\n          }\n\n          setSuggestions(result.predictions || []);\n          setShowSuggestions(result.predictions?.length > 0);\n          setSelectedIndex(-1);\n        } catch (error) {\n          if (requestId !== requestIdRef.current) return;\n          console.error('Error searching addresses:', error);\n          setSuggestions([]);\n          setShowSuggestions(false);\n        }\n      });\n    }, DEBOUNCE_TIME);\n\n    return () => {\n      if (debounceTimerRef.current) {\n        clearTimeout(debounceTimerRef.current);\n      }\n      requestIdRef.current++;\n    };\n  }, [searchQuery, country]);\n\n  const handleSelectAddress = (suggestion: AddressSuggestion) => {\n    setShowSuggestions(false);\n    setSelectedIndex(-1);\n    setSearchQuery('');\n\n    startTransition(async () => {\n      const details = await getPlaceDetails({\n        placeId: suggestion.placeId,\n        fields: ['address_components', 'formatted_address', 'geometry'],\n        language: LANGUAGE_RESULT,\n      });\n\n      if (details?.result) {\n        const addressComponents = details.result.address_components || [];\n\n        let streetValue = '';\n        let cityValue = '';\n        let postalCodeValue = '';\n        let countryValue = '';\n\n        addressComponents.forEach((component: any) => {\n          const types = component.types;\n\n          if (types.includes('street_number')) {\n            streetValue = `${component.long_name} ${streetValue}`;\n          }\n          if (types.includes('route')) {\n            streetValue = `${streetValue} ${component.long_name}`;\n          }\n          if (types.includes('locality') || types.includes('administrative_area_level_2')) {\n            cityValue = component.long_name;\n          }\n          if (types.includes('postal_code')) {\n            postalCodeValue = component.long_name;\n          }\n          if (types.includes('country')) {\n            countryValue = component.long_name;\n          }\n        });\n\n        street.field.onChange(streetValue.trim());\n        city.field.onChange(cityValue.trim());\n        postalCode.field.onChange(postalCodeValue.trim());\n        countryField.field.onChange(countryValue.trim());\n        fullAddress.field.onChange(details.result.formatted_address.trim());\n        placeId.field.onChange(suggestion.placeId.trim());\n      } else {\n        fullAddress.field.onChange(suggestion.description.trim());\n        placeId.field.onChange(suggestion.placeId.trim());\n      }\n    });\n  };\n\n  const handleKeyDown = (e: React.KeyboardEvent) => {\n    if (!showSuggestions || suggestions.length === 0) return;\n\n    switch (e.key) {\n      case 'ArrowDown':\n        e.preventDefault();\n        setSelectedIndex((prev) => (prev < suggestions.length - 1 ? prev + 1 : 0));\n        break;\n      case 'ArrowUp':\n        e.preventDefault();\n        setSelectedIndex((prev) => (prev > 0 ? prev - 1 : suggestions.length - 1));\n        break;\n      case 'Enter':\n        e.preventDefault();\n        if (selectedIndex >= 0 && selectedIndex < suggestions.length) {\n          handleSelectAddress(suggestions[selectedIndex]);\n        }\n        break;\n      case 'Escape':\n        e.preventDefault();\n        setShowSuggestions(false);\n        setSelectedIndex(-1);\n        inputRef.current?.blur();\n        break;\n    }\n  };\n\n  const handleFocus = () => {\n    if (suggestions.length > 0 && searchQuery.length >= 3) {\n      setShowSuggestions(true);\n    }\n  };\n\n  const handleBlur = (e: React.FocusEvent) => {\n    const relatedTarget = e.relatedTarget as HTMLElement;\n    if (popoverRef.current?.contains(relatedTarget)) {\n      return;\n    }\n\n    fullAddress.field.onBlur();\n\n    setTimeout(() => {\n      setShowSuggestions(false);\n      setSelectedIndex(-1);\n    }, 150);\n  };\n\n  const fullAddressInvalid = fullAddress.fieldState.invalid;\n  const id = props.id ?? fullAddress.field.name;\n\n  return (\n    <Field className='gap-2' data-invalid={fullAddressInvalid}>\n      <FieldLabel htmlFor={id} className='flex items-center justify-between'>\n        {label}\n        {fullAddressInvalid && (\n          <FieldError className='max-sm:hidden text-xs opacity-80' errors={[fullAddress.fieldState.error]} />\n        )}\n      </FieldLabel>\n      <div className='relative'>\n        <Popover open={showSuggestions} onOpenChange={setShowSuggestions}>\n          <PopoverTrigger asChild>\n            <div className='relative'>\n              <Input\n                ref={inputRef}\n                name={fullAddress.field.name}\n                value={fullAddress.field.value ?? ''}\n                placeholder={placeholder}\n                autoComplete='off'\n                {...props}\n                id={id}\n                onChange={(e) => {\n                  const value = e.target.value;\n                  fullAddress.field.onChange(value);\n                  setSearchQuery(value);\n                }}\n                onFocus={handleFocus}\n                onBlur={handleBlur}\n                onKeyDown={handleKeyDown}\n                aria-invalid={fullAddressInvalid}\n              />\n              <div className='absolute inset-y-0 right-0 flex items-center pr-3'>\n                {isPending ? (\n                  <Loader2 className='size-4 animate-spin text-muted-foreground' />\n                ) : (\n                  <MapPin className='size-4 text-muted-foreground' />\n                )}\n              </div>\n            </div>\n          </PopoverTrigger>\n          <PopoverContent\n            ref={popoverRef}\n            className='p-0'\n            align='start'\n            onOpenAutoFocus={(e) => e.preventDefault()}\n            style={{ width: 'var(--radix-popover-trigger-width)' }}\n          >\n            <Command className='w-full'>\n              <CommandList className='max-h-60'>\n                <CommandEmpty>{isPending ? 'Searching...' : 'No addresses found'}</CommandEmpty>\n                <CommandGroup>\n                  {suggestions.map((suggestion, index) => (\n                    <CommandItem\n                      key={suggestion.placeId}\n                      value={suggestion.description}\n                      onSelect={() => handleSelectAddress(suggestion)}\n                      className={cn(\n                        'flex items-start space-x-2 p-3 cursor-pointer',\n                        selectedIndex === index && 'bg-accent',\n                      )}\n                    >\n                      <MapPin className='size-4 mt-0.5 text-muted-foreground flex-shrink-0' />\n                      <div className='flex-1 min-w-0'>\n                        <div className='font-medium text-sm'>{suggestion.mainText}</div>\n                        <div className='text-xs text-muted-foreground truncate'>{suggestion.secondaryText}</div>\n                      </div>\n                    </CommandItem>\n                  ))}\n                </CommandGroup>\n              </CommandList>\n            </Command>\n          </PopoverContent>\n        </Popover>\n      </div>\n      {description && <p className='text-sm text-muted-foreground'>{description}</p>}\n      {fullAddressInvalid && (\n        <FieldError className='sm:hidden text-xs text-left opacity-80' errors={[fullAddress.fieldState.error]} />\n      )}\n    </Field>\n  );\n}\n"
    },
    {
      "path": "./items/react-hook-form/address-field/extras/places.action.ts",
      "type": "registry:file",
      "target": "./actions/shuip/places.ts",
      "content": "'use server';\n\nconst GOOGLE_PLACES_API_KEY = process.env.GOOGLE_PLACES_API_KEY;\n\nexport interface AutocompleteParams {\n  input: string;\n  components?: string;\n  types?: string;\n  language?: string;\n}\n\nexport async function getPlacesAutocomplete({ input, components, types, language = 'fr' }: AutocompleteParams) {\n  try {\n    if (!GOOGLE_PLACES_API_KEY) {\n      throw new Error('Google Places API key not configured');\n    }\n\n    if (!input || input.length < 3) {\n      return { predictions: [] };\n    }\n\n    // Construire l'URL de l'API Google Places Autocomplete\n    const params = new URLSearchParams({\n      input,\n      key: GOOGLE_PLACES_API_KEY,\n      language,\n    });\n\n    if (components) {\n      params.append('components', components);\n    }\n\n    if (types) {\n      params.append('types', types);\n    }\n\n    const response = await fetch(`https://maps.googleapis.com/maps/api/place/autocomplete/json?${params}`, {\n      method: 'GET',\n      headers: {\n        'Content-Type': 'application/json',\n      },\n    });\n\n    if (!response.ok) {\n      throw new Error(`Google Places API error: ${response.status}`);\n    }\n\n    const data = await response.json();\n\n    if (data.status !== 'OK' && data.status !== 'ZERO_RESULTS') {\n      throw new Error(`Google Places API error: ${data.status}`);\n    }\n\n    // Transformer les données pour notre format\n    const predictions =\n      data.predictions?.map((prediction: any) => ({\n        placeId: prediction.place_id,\n        description: prediction.description,\n        mainText: prediction.structured_formatting?.main_text || prediction.description,\n        secondaryText: prediction.structured_formatting?.secondary_text || '',\n        types: prediction.types || [],\n      })) || [];\n\n    return { predictions };\n  } catch (error) {\n    return { error: error instanceof Error ? error.message : 'Internal server error', predictions: [] };\n  }\n}\n\nexport interface PlaceDetailsParams {\n  placeId: string;\n  fields?: string[];\n  language?: string;\n}\n\nexport async function getPlaceDetails({ placeId, fields, language = 'fr' }: PlaceDetailsParams) {\n  try {\n    if (!GOOGLE_PLACES_API_KEY) {\n      throw new Error('Google Places API key not configured');\n    }\n\n    if (!placeId) {\n      throw new Error('Place ID is required');\n    }\n\n    const params = new URLSearchParams({\n      place_id: placeId,\n      key: GOOGLE_PLACES_API_KEY,\n      language,\n    });\n\n    if (fields && Array.isArray(fields)) {\n      params.append('fields', fields.join(','));\n    }\n\n    const response = await fetch(`https://maps.googleapis.com/maps/api/place/details/json?${params}`, {\n      method: 'GET',\n      headers: {\n        'Content-Type': 'application/json',\n      },\n    });\n\n    if (!response.ok) {\n      throw new Error(`Google Places API error: ${response.status}`);\n    }\n\n    const data = await response.json();\n\n    if (data.status !== 'OK') {\n      throw new Error(`Google Places API error: ${data.status}`);\n    }\n\n    return data;\n  } catch (error) {\n    return { error: error instanceof Error ? error.message : 'Internal server error' };\n  }\n}\n"
    }
  ],
  "$schema": "https://ui.shadcn.com/schema/registry-item.json"
}
