# บทนำ

> react-thaizip เพิ่มคอมโพเนนต์กรอกที่อยู่ไทยลงในโปรเจกต์ React และ Next.js โดยคัดลอกซอร์สโค้ดมาให้คุณเป็นเจ้าของ

Source: https://react-thai-zip.vercel.app/docs

```tsx title="autocomplete-demo.tsx"
'use client'

import { ThaiAddressAutocomplete } from '@/components/thai-address-autocomplete'

export default function AutocompleteDemo() {
  return <ThaiAddressAutocomplete onValueChange={(address) => console.log(address)} />
}
```

**react-thaizip ไม่ใช่ component library ที่ติดตั้งเป็น dependency** แต่เป็น CLI ที่คัดลอกซอร์สโค้ดคอมโพเนนต์ที่อยู่ไทยลงในโปรเจกต์ของคุณ แบบเดียวกับ [shadcn/ui](https://ui.shadcn.com/) เมื่อเพิ่มแล้ว ไฟล์เหล่านั้นเป็นโค้ดของคุณ แก้ markup, style หรือพฤติกรรมได้โดยตรง

ตัวอย่างด้านบนคือ `ThaiAddressAutocomplete` จาก template จริงที่ CLI คัดลอกให้ ไม่ใช่สำเนาที่เขียนขึ้นสำหรับเว็บเอกสาร กด "ดูโค้ด" เพื่อดูโค้ดที่ใช้

## หลักการ

- **เป็นเจ้าของโค้ด**: คอมโพเนนต์อยู่ในโปรเจกต์ของคุณ ไม่ต้อง override style หรือห่อ wrapper เพื่อปรับแต่ง
- **ข้อมูลครบในตัว**: ค้นหาตำบล อำเภอ จังหวัด และรหัสไปรษณีย์ทั้งภาษาไทยและอังกฤษด้วย [thaizip](https://naay99999.github.io/thai-zip/) โดยไม่ต้องเรียก API ภายนอก
- **เข้ากับ design system ที่มีอยู่**: ได้แบบ vanilla (Base UI + Tailwind) หรือแบบที่ประกอบจาก primitives ของ shadcn/ui (Base UI, Radix หรือ React Aria) ตามที่ `init` ตรวจพบ
- **พร้อมใช้กับ AI**: ทุกหน้ามีฉบับ Markdown พร้อมโค้ดตัวอย่าง รวมถึง [`/llms.txt`](https://react-thai-zip.vercel.app/llms.txt) และ [`/llms-full.txt`](https://react-thai-zip.vercel.app/llms-full.txt) ดู [ใช้ react-thaizip กับ AI](https://react-thai-zip.vercel.app/docs/guides/using-with-ai)

## คอมโพเนนต์

| คอมโพเนนต์ | ใช้เมื่อ |
| --- | --- |
| [Autocomplete](https://react-thai-zip.vercel.app/docs/components/autocomplete) | ให้ผู้ใช้พิมพ์ค้นหาด้วยชื่อตำบล อำเภอ จังหวัด หรือรหัสไปรษณีย์ |
| [Cascade select](https://react-thai-zip.vercel.app/docs/components/cascade-select) | ให้ผู้ใช้เลือกจังหวัด → อำเภอ/เขต → ตำบล/แขวง ตามลำดับ |
| [Address form](https://react-thai-zip.vercel.app/docs/components/address-form) | ต้องการบ้านเลขที่ หมู่ ซอย ถนน พร้อมที่อยู่ตามลำดับชั้นในฟอร์มเดียว |
| [Address display](https://react-thai-zip.vercel.app/docs/components/address-display) | แสดงที่อยู่ที่บันทึกไว้แบบอ่านอย่างเดียว |
| [Address form field](https://react-thai-zip.vercel.app/docs/components/address-form-field) | ผูก cascade select เข้ากับ react-hook-form |

## เริ่มอย่างรวดเร็ว

```npm
npx react-thaizip init
npx react-thaizip add autocomplete
```

`init` ตรวจ Tailwind และ shadcn/ui แล้วสร้าง `thaizip.config.json` ส่วน `add` คัดลอกคอมโพเนนต์พร้อมไฟล์ช่วยเหลือและติดตั้ง dependencies ดูรายละเอียดทีละขั้นที่ [เริ่มต้นใช้งาน](https://react-thai-zip.vercel.app/docs/getting-started)

ต้องการ API ค้นหาที่อยู่ระดับล่างเพื่อสร้าง UI เอง ดู [thaizip Core API](https://naay99999.github.io/thai-zip/)

---

# เริ่มต้นใช้งาน

> Scaffold คอมโพเนนต์ที่อยู่ไทยชิ้นแรกในโปรเจกต์ของคุณ

Source: https://react-thai-zip.vercel.app/docs/getting-started

react-thaizip เป็น CLI สำหรับ scaffold ไม่ใช่แพ็กเกจคอมโพเนนต์ runtime: มันเขียนซอร์สโค้ดคอมโพเนนต์ลงในโปรเจกต์ของคุณ และคุณเป็นเจ้าของซอร์สที่สร้างขึ้นทั้งหมด

## สิ่งที่ต้องมี

ใช้ Node.js 18 ขึ้นไป และโปรเจกต์ React หรือ Next.js ที่ติดตั้ง Tailwind CSS v3 หรือ v4 ไว้แล้ว `init` ตรวจหา Tailwind แต่จะไม่ติดตั้งให้ หากตรวจไม่พบ ให้ทำตาม [ขั้นตอนแก้ปัญหา Tailwind](https://react-thai-zip.vercel.app/docs/troubleshooting#init-ตรวจไม่พบ-tailwind)

## สร้างการตั้งค่า

```npm
npx react-thaizip init
```

คำสั่งนี้ตรวจสอบโครงสร้างโปรเจกต์และ Tailwind เลือก package manager แล้วสร้าง `thaizip.config.json` ไฟล์นี้บันทึกตำแหน่งคอมโพเนนต์, hook และ `lib` ที่คำสั่ง `add` จะใช้ ดูทุกฟิลด์และกฎการตรวจหาได้ที่ [ข้อมูลอ้างอิงการตั้งค่า](https://react-thai-zip.vercel.app/docs/reference/config)

ถ้าโปรเจกต์มี `components.json` ของ shadcn/ui แบบที่รองรับ `init` จะบันทึกด้วยว่า primitives ใช้ Base UI, Radix หรือ React Aria หากไม่มีจะเลือกคอมโพเนนต์แบบ vanilla Base UI

`init` เพิ่ม design tokens แบบ CSS custom properties ลงใน global stylesheet ด้วย สำหรับ Tailwind v3 จะพิมพ์ `theme.extend` snippet ที่ต้องนำไปเพิ่มในไฟล์ตั้งค่า Tailwind เอง

## เพิ่ม autocomplete

```npm
npx react-thaizip add autocomplete
```

โดยค่าเริ่มต้น ไฟล์ที่สร้างจะมีหน้าตาเช่นนี้ (ตำแหน่งคอมโพเนนต์อาจต่างกันตามโครงสร้างโปรเจกต์หรือการเลือกใน `init`):

```text
components/
  thai-address-autocomplete.tsx
hooks/
  use-thai-address-index.ts
lib/
  utils.ts
```

คำสั่งยังติดตั้ง dependency ที่คอมโพเนนต์ต้องใช้หากยังไม่มี ดู targets, aliases และ flags เพิ่มเติมได้ที่ [ข้อมูลอ้างอิง CLI](https://react-thai-zip.vercel.app/docs/reference/cli)

ใช้ import path ที่ `add` พิมพ์ให้ เพราะ path นั้นตรงกับ directory ที่เลือกตอน `init` ตัวอย่างในเอกสารนี้ใช้ alias `@/components/...` ตามแบบ shadcn/ui หากโปรเจกต์ไม่มี alias นี้หรือเก็บไฟล์ไว้ที่อื่น ให้เปลี่ยนเป็น path ที่ `add` พิมพ์

## ใช้คอมโพเนนต์ครั้งแรก

```tsx
import { ThaiAddressAutocomplete } from '@/components/thai-address-autocomplete'

export function AddressField() {
  return <ThaiAddressAutocomplete onValueChange={(address) => console.log(address)} />
}
```

ต่อไป ดูคู่มือ [cascade select](https://react-thai-zip.vercel.app/docs/components/cascade-select) เพื่อเลือก UI สำหรับการกรอกที่อยู่ตามลำดับ และดู [การใช้งานร่วมกับฟอร์ม](https://react-thai-zip.vercel.app/docs/guides/forms) เพื่อส่งค่าที่อยู่ด้วยฟอร์มของเบราว์เซอร์

---

# คอมโพเนนต์ทั้งหมด

> คอมโพเนนต์ที่อยู่ไทยทั้งหมดที่ CLI เพิ่มลงโปรเจกต์ได้

Source: https://react-thai-zip.vercel.app/docs/components

เพิ่มคอมโพเนนต์ใดก็ได้ด้วย `npx react-thaizip add <name>` ทุกตัวยกเว้นตัวแสดงที่อยู่แบบอ่านอย่างเดียวมี style แบบ vanilla และอีกหนึ่ง style ต่อ base ของ shadcn/ui (Base UI, Radix UI, React Aria) เลือก style ได้ที่ด้านบนของหน้าคอมโพเนนต์เพื่อดูตัวอย่าง

<Cards>
  <Card title="Autocomplete" href="https://react-thai-zip.vercel.app/docs/components/autocomplete">
    พิมพ์ค้นหาด้วยตำบล อำเภอ/เขต จังหวัด หรือรหัสไปรษณีย์
  </Card>
  <Card title="Cascade select" href="https://react-thai-zip.vercel.app/docs/components/cascade-select">
    เลือกจังหวัด → อำเภอ/เขต → ตำบล/แขวง ตามลำดับ
  </Card>
  <Card title="ฟอร์มที่อยู่แบบเต็ม" href="https://react-thai-zip.vercel.app/docs/components/address-form">
    บ้านเลขที่ หมู่ ซอย ถนน พร้อมเลือกที่อยู่ตามลำดับชั้นในฟอร์มเดียว
  </Card>
  <Card title="แสดงที่อยู่" href="https://react-thai-zip.vercel.app/docs/components/address-display">
    แสดงที่อยู่ที่บันทึกไว้แบบอ่านอย่างเดียว ไม่ต้องโหลดดัชนีที่อยู่
  </Card>
  <Card title="ฟิลด์ react-hook-form" href="https://react-thai-zip.vercel.app/docs/components/address-form-field">
    cascade select ที่ผูกกับ validation และ state ของ react-hook-form
  </Card>
</Cards>

---

# ค้นหาที่อยู่แบบ autocomplete

> ค้นหาที่อยู่ไทยจากข้อความที่พิมพ์ พร้อมค่าที่อยู่แบบมีโครงสร้าง

Source: https://react-thai-zip.vercel.app/docs/components/autocomplete

`ThaiAddressAutocomplete` เหมาะกับหน้าฟอร์มที่ผู้ใช้ต้องการค้นหาที่อยู่ด้วยตำบล อำเภอ/เขต จังหวัด หรือรหัสไปรษณีย์

```tsx title="autocomplete-demo.tsx"
'use client'

import { ThaiAddressAutocomplete } from '@/components/thai-address-autocomplete'

export default function AutocompleteDemo() {
  return <ThaiAddressAutocomplete onValueChange={(address) => console.log(address)} />
}
```

## ติดตั้ง

**CLI**

```bash
npx react-thaizip add autocomplete
```

CLI เลือกไฟล์ให้ตาม style ที่ `init` บันทึกไว้: โปรเจกต์ที่มี `components.json` ของ shadcn/ui จะได้ไฟล์ที่ประกอบจาก primitives ของ shadcn (Base UI, Radix หรือ React Aria) นอกนั้นได้แบบ vanilla

**ติดตั้งเอง — Vanilla**

1. ติดตั้ง dependencies: `npm install clsx tailwind-merge thaizip @base-ui/react`
1. คัดลอกไฟล์ต่อไปนี้ลงในโปรเจกต์:
   - `lib/utils.ts` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/lib/utils.ts)
   - `hooks/use-thai-address-index.ts` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-index.ts)
   - `components/thai-address-autocomplete.tsx` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/thai-address-autocomplete.tsx)
1. แก้ import `@/lib/...` และ `@/hooks/...` ให้ตรงกับตำแหน่งไฟล์ในโปรเจกต์ (CLI แก้ให้อัตโนมัติ)
1. แบบ vanilla ต้องมี design tokens ใน global stylesheet ด้วย ดู [การปรับแต่ง](https://react-thai-zip.vercel.app/docs/guides/customization#tokens-ที่-init-เพิ่ม)

**ติดตั้งเอง — shadcn/ui · Base UI**

1. ติดตั้ง dependencies: `npm install clsx tailwind-merge thaizip`
1. เพิ่ม shadcn/ui primitives ที่คอมโพเนนต์ใช้: `npx shadcn@latest add popover command button`
1. คัดลอกไฟล์ต่อไปนี้ลงในโปรเจกต์:
   - `lib/utils.ts` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/lib/utils.ts)
   - `hooks/use-thai-address-index.ts` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-index.ts)
   - `components/thai-address-autocomplete.tsx` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/shadcn/base/thai-address-autocomplete.tsx)
1. แก้ import `@/lib/...` และ `@/hooks/...` ให้ตรงกับตำแหน่งไฟล์ในโปรเจกต์ (CLI แก้ให้อัตโนมัติ)

**ติดตั้งเอง — shadcn/ui · Radix**

1. ติดตั้ง dependencies: `npm install clsx tailwind-merge thaizip`
1. เพิ่ม shadcn/ui primitives ที่คอมโพเนนต์ใช้: `npx shadcn@latest add popover command button`
1. คัดลอกไฟล์ต่อไปนี้ลงในโปรเจกต์:
   - `lib/utils.ts` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/lib/utils.ts)
   - `hooks/use-thai-address-index.ts` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-index.ts)
   - `components/thai-address-autocomplete.tsx` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/shadcn/radix/thai-address-autocomplete.tsx)
1. แก้ import `@/lib/...` และ `@/hooks/...` ให้ตรงกับตำแหน่งไฟล์ในโปรเจกต์ (CLI แก้ให้อัตโนมัติ)

**ติดตั้งเอง — shadcn/ui · React Aria**

1. ติดตั้ง dependencies: `npm install clsx tailwind-merge thaizip`
1. เพิ่ม shadcn/ui primitives ที่คอมโพเนนต์ใช้: `npx shadcn@latest add popover command button`
1. คัดลอกไฟล์ต่อไปนี้ลงในโปรเจกต์:
   - `lib/utils.ts` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/lib/utils.ts)
   - `hooks/use-thai-address-index.ts` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-index.ts)
   - `components/thai-address-autocomplete.tsx` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/shadcn/aria/thai-address-autocomplete.tsx)
1. แก้ import `@/lib/...` และ `@/hooks/...` ให้ตรงกับตำแหน่งไฟล์ในโปรเจกต์ (CLI แก้ให้อัตโนมัติ)

## วิธีใช้

```tsx
import { ThaiAddressAutocomplete } from '@/components/thai-address-autocomplete'
```

```tsx
<ThaiAddressAutocomplete onValueChange={(address) => console.log(address)} />
```

ค่าใน `onValueChange` คือ `ResolvedThaiAddress | null` เมื่อผู้ใช้เลือกคำแนะนำจะได้ที่อยู่แบบมีโครงสร้าง และการพิมพ์ให้ข้อความว่างจะล้างที่อยู่ที่เลือกอยู่แล้วด้วยค่า `null`

## ตัวอย่าง

### ค้นหาภาษาอังกฤษ

ใช้ `locale="en"` เพื่อแสดงป้ายกำกับ คำใบ้ และผลลัพธ์เป็นภาษาอังกฤษ ข้อมูลที่ส่งกลับยังมีชื่อทั้งไทยและอังกฤษ

```tsx title="autocomplete-en.tsx"
'use client'

import { ThaiAddressAutocomplete } from '@/components/thai-address-autocomplete'

export default function AutocompleteEn() {
  return <ThaiAddressAutocomplete locale="en" onValueChange={(address) => console.log(address)} />
}
```

### ป้ายกำกับ

คอมโพเนนต์ไม่มีป้ายกำกับในตัว ครอบด้วย `<label>` เพื่อให้ช่องมีชื่อที่ screen reader อ่านได้ และเพิ่มคำอธิบายไว้ด้านล่าง

```tsx title="autocomplete-label.tsx"
import { ThaiAddressAutocomplete } from '@/components/thai-address-autocomplete'

export default function AutocompleteLabel() {
  return (
    <div className="flex flex-col gap-2">
      <label className="flex flex-col gap-2 text-sm font-medium">
        ที่อยู่จัดส่ง
        <ThaiAddressAutocomplete />
      </label>
      <p className="text-sm text-muted-foreground">พิมพ์ตำบล อำเภอ หรือรหัสไปรษณีย์ แล้วเลือกจากรายการ</p>
    </div>
  )
}
```

### ควบคุมค่าจากภายนอก

ส่ง `value` เพื่อควบคุมค่า หรือส่ง `defaultValue` เพื่อตั้งค่าเริ่มต้นสำหรับโหมด uncontrolled ปุ่มในตัวอย่างนี้ล้างค่าที่ควบคุมอยู่และแสดงผลลัพธ์ที่ callback ส่งกลับมา

```tsx title="autocomplete-controlled.tsx"
'use client'

import { useState } from 'react'
import type { ResolvedThaiAddress } from 'thaizip'
import { ThaiAddressAutocomplete } from '@/components/thai-address-autocomplete'

export default function AutocompleteControlled() {
  const [address, setAddress] = useState<ResolvedThaiAddress | null>(null)

  return (
    <div className="flex flex-col gap-3">
      <ThaiAddressAutocomplete value={address} onValueChange={setAddress} />
      <div>
        <button type="button" className="rounded-md border border-input bg-background px-3 py-1.5 text-sm hover:bg-accent" onClick={() => setAddress(null)}>
          ล้างค่า
        </button>
      </div>
      <p className="text-sm font-medium">ค่าปัจจุบัน</p>
      <pre className="max-h-72 overflow-auto rounded-md border bg-muted p-3 text-xs">{JSON.stringify(address, null, 2)}</pre>
    </div>
  )
}
```

### ปิดใช้งาน

ส่ง `disabled` เพื่อปิดช่องค้นหา

```tsx title="autocomplete-disabled.tsx"
'use client'

import { ThaiAddressAutocomplete } from '@/components/thai-address-autocomplete'

export default function AutocompleteDisabled() {
  return <ThaiAddressAutocomplete disabled />
}
```

### ไม่ผ่านการตรวจสอบ

ส่ง `aria-invalid` เพื่อแสดงว่าช่องนี้ไม่ผ่านการตรวจสอบ เช่นหลังตรวจฟอร์มไม่ผ่าน

```tsx title="autocomplete-invalid.tsx"
'use client'

import { ThaiAddressAutocomplete } from '@/components/thai-address-autocomplete'

export default function AutocompleteInvalid() {
  return <ThaiAddressAutocomplete aria-invalid />
}
```

### ปรับข้อความ

ใช้ `texts` เพื่อแทนที่ placeholder และข้อความตอนโหลด/ผิดพลาด/ไม่พบผลลัพธ์ คีย์ที่ไม่ได้ส่งจะใช้ค่าเริ่มต้นตาม `locale`

```tsx title="autocomplete-texts.tsx"
import { ThaiAddressAutocomplete } from '@/components/thai-address-autocomplete'

export default function AutocompleteTexts() {
  return (
    <ThaiAddressAutocomplete
      texts={{
        placeholder: 'ค้นหาจากรหัสไปรษณีย์ เช่น 10110',
        emptyText: 'ไม่พบที่อยู่นี้ ลองพิมพ์ชื่ออำเภอแทน',
      }}
    />
  )
}
```

### ปรับสไตล์

แต่ละส่วนของคอมโพเนนต์มี class prop ของตัวเอง (ดู [API Reference](#api-reference)) class ที่ส่งจะรวมด้วย `cn` จึงทับค่าเริ่มต้นได้

```tsx title="autocomplete-styled.tsx"
import { ThaiAddressAutocomplete } from '@/components/thai-address-autocomplete'

export default function AutocompleteStyled() {
  return (
    <ThaiAddressAutocomplete
      inputClassName="rounded-full"
      popupClassName="rounded-2xl"
      itemClassName="rounded-xl"
    />
  )
}
```

### ใช้ในฟอร์ม

ส่ง `name` เพื่อส่งที่อยู่ที่เลือกไปกับฟอร์มปกติ คอมโพเนนต์จะสร้าง hidden inputs สี่ช่อง (`address-subdistrict`, `address-district`, `address-province`, `address-zipcode`) และ `required` จะกันไม่ให้ส่งฟอร์มจนกว่าจะเลือกที่อยู่ ดูรูปแบบอื่นได้ที่ [การใช้งานร่วมกับฟอร์ม](https://react-thai-zip.vercel.app/docs/guides/forms)

```tsx title="form-autocomplete.tsx"
'use client'

import { useRef, useState, type FormEvent } from 'react'
import { ThaiAddressAutocomplete } from '@/components/thai-address-autocomplete'

export default function FormAutocomplete() {
  const ref = useRef<HTMLInputElement>(null)
  const [payload, setPayload] = useState<Record<string, FormDataEntryValue> | null>(null)

  function handleSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault()
    setPayload(Object.fromEntries(new FormData(event.currentTarget)))
  }

  return (
    <form className="flex flex-col gap-3" onSubmit={handleSubmit}>
      <ThaiAddressAutocomplete
        ref={ref}
        name="address"
        required
      />
      <div className="flex gap-2">
        <button type="button" className="rounded-md border border-input bg-background px-3 py-1.5 text-sm hover:bg-accent" onClick={() => ref.current?.focus()}>
          โฟกัสช่องที่อยู่
        </button>
        <button type="submit" className="rounded-md border border-input bg-background px-3 py-1.5 text-sm hover:bg-accent">
          ส่งฟอร์ม
        </button>
      </div>
      <p className="text-sm font-medium">FormData ที่ส่ง</p>
      <pre className="max-h-72 overflow-auto rounded-md border bg-muted p-3 text-xs">{JSON.stringify(payload, null, 2)}</pre>
    </form>
  )
}
```

## API Reference

| Prop | Type | ค่าเริ่มต้น | รายละเอียด |
| --- | --- | --- | --- |
| `value` | `ResolvedThaiAddress \| null` | — | ค่าที่ควบคุมอยู่; ส่ง `null` เพื่อล้าง |
| `defaultValue` | `ResolvedThaiAddress \| null` | — | ค่าเริ่มต้นของโหมด uncontrolled และเติมข้อความค้นหาเริ่มต้น |
| `onValueChange` | `(address: ResolvedThaiAddress \| null) => void` | — | เรียกเมื่อเลือกหรือล้างที่อยู่ |
| `name` | `string` | — | สร้าง hidden inputs สี่ช่อง: `<name>-subdistrict`, `-district`, `-province`, `-zipcode` |
| `locale` | `'th' \| 'en'` | `'th'` | ภาษาของผลลัพธ์และข้อความเริ่มต้น |
| `texts` | `Partial<ThaiAddressAutocompleteTexts>` | — | แทนที่ `placeholder`, `clearAriaLabel`, `loadingText`, `errorText`, `retryLabel`, `emptyText` |
| `indexLoad` | `'mount' \| 'visible'` | `'mount'` | โหลดข้อมูลที่อยู่ตอน mount หรือรอจนช่องเลื่อนเข้ามาในจอ |
| `limit` | `number` | `10` | จำนวนคำแนะนำสูงสุด |
| `debounce` | `number` | `200` | ระยะหน่วงก่อนค้นหา (มิลลิวินาที) |
| `threshold` | `number` | `0.4` | คะแนนความใกล้เคียงขั้นต่ำ (0–1) ที่คำแนะนำจะถูกแสดง |
| `disabled` | `boolean` | `false` | ปิดใช้งานช่องค้นหา |
| `required` | `boolean` | — | กำหนดให้ช่องต้องมีค่า |
| `aria-invalid` | `React.AriaAttributes['aria-invalid']` | — | ระบุสถานะไม่ผ่านการตรวจสอบ |
| `onBlur` | `React.FocusEventHandler` | — | Vanilla: ทำงานเมื่อ input สูญเสียโฟกัส ส่วน shadcn/ui: ทำงานที่ปุ่ม trigger (`HTMLButtonElement`) |
| `onError` | `(error: Error) => void` | — | ทำงานเมื่อโหลดดัชนีที่อยู่ไม่สำเร็จ |
| `className` | `string` | — | wrapper หลัก |
| `inputClassName` | `string` | — | ช่องพิมพ์ค้นหา |
| `popupClassName` | `string` | — | กล่องผลลัพธ์ |
| `itemClassName` | `string` | — | แต่ละผลลัพธ์ |
| `ref` | `React.Ref<HTMLInputElement>` | — | ส่งต่อไปยัง input ใน style shadcn/ui นั้น input อยู่ใน popover จึงเป็น `null` ระหว่างที่ popover ปิดอยู่ |

`limit`, `debounce` และ `threshold` ส่งต่อตรงไปยัง hook `useThaiAddressAutocomplete` ของ thaizip

## ดูเพิ่มเติม

ดู [การใช้งานร่วมกับฟอร์ม](https://react-thai-zip.vercel.app/docs/guides/forms) สำหรับการส่งค่าที่อยู่ และ [การปรับแต่ง](https://react-thai-zip.vercel.app/docs/guides/customization) สำหรับปรับหน้าตา ส่วน UI แบบเลือกตามลำดับ ดู [cascade select](https://react-thai-zip.vercel.app/docs/components/cascade-select)

---

# เลือกที่อยู่แบบลำดับชั้น

> เลือกจังหวัด อำเภอหรือเขต และตำบลหรือแขวงตามลำดับ

Source: https://react-thai-zip.vercel.app/docs/components/cascade-select

`ThaiAddressCascadeSelect` เหมาะกับฟอร์มที่ต้องการให้ผู้ใช้เลือกที่อยู่ตามลำดับ จังหวัด → อำเภอ/เขต → ตำบล/แขวง

```tsx title="cascade-select-demo.tsx"
'use client'

import { ThaiAddressCascadeSelect } from '@/components/thai-address-cascade-select'

export default function CascadeSelectDemo() {
  return <ThaiAddressCascadeSelect onValueChange={(address) => console.log(address)} />
}
```

## ติดตั้ง

**CLI**

```bash
npx react-thaizip add cascade-select
```

CLI เลือกไฟล์ให้ตาม style ที่ `init` บันทึกไว้: โปรเจกต์ที่มี `components.json` ของ shadcn/ui จะได้ไฟล์ที่ประกอบจาก primitives ของ shadcn (Base UI, Radix หรือ React Aria) นอกนั้นได้แบบ vanilla

**ติดตั้งเอง — Vanilla**

1. ติดตั้ง dependencies: `npm install clsx tailwind-merge thaizip @base-ui/react`
1. คัดลอกไฟล์ต่อไปนี้ลงในโปรเจกต์:
   - `lib/utils.ts` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/lib/utils.ts)
   - `hooks/use-thai-address-index.ts` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-index.ts)
   - `hooks/use-thai-address-cascade.ts` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-cascade.ts)
   - `components/thai-address-cascade-select.tsx` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/thai-address-cascade-select.tsx)
1. แก้ import `@/lib/...` และ `@/hooks/...` ให้ตรงกับตำแหน่งไฟล์ในโปรเจกต์ (CLI แก้ให้อัตโนมัติ)
1. แบบ vanilla ต้องมี design tokens ใน global stylesheet ด้วย ดู [การปรับแต่ง](https://react-thai-zip.vercel.app/docs/guides/customization#tokens-ที่-init-เพิ่ม)

**ติดตั้งเอง — shadcn/ui · Base UI**

1. ติดตั้ง dependencies: `npm install clsx tailwind-merge thaizip`
1. เพิ่ม shadcn/ui primitives ที่คอมโพเนนต์ใช้: `npx shadcn@latest add select label button input`
1. คัดลอกไฟล์ต่อไปนี้ลงในโปรเจกต์:
   - `lib/utils.ts` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/lib/utils.ts)
   - `hooks/use-thai-address-index.ts` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-index.ts)
   - `hooks/use-thai-address-cascade.ts` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-cascade.ts)
   - `components/thai-address-cascade-select.tsx` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/shadcn/base/thai-address-cascade-select.tsx)
1. แก้ import `@/lib/...` และ `@/hooks/...` ให้ตรงกับตำแหน่งไฟล์ในโปรเจกต์ (CLI แก้ให้อัตโนมัติ)

**ติดตั้งเอง — shadcn/ui · Radix**

1. ติดตั้ง dependencies: `npm install clsx tailwind-merge thaizip`
1. เพิ่ม shadcn/ui primitives ที่คอมโพเนนต์ใช้: `npx shadcn@latest add select label button input`
1. คัดลอกไฟล์ต่อไปนี้ลงในโปรเจกต์:
   - `lib/utils.ts` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/lib/utils.ts)
   - `hooks/use-thai-address-index.ts` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-index.ts)
   - `hooks/use-thai-address-cascade.ts` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-cascade.ts)
   - `components/thai-address-cascade-select.tsx` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/shadcn/radix/thai-address-cascade-select.tsx)
1. แก้ import `@/lib/...` และ `@/hooks/...` ให้ตรงกับตำแหน่งไฟล์ในโปรเจกต์ (CLI แก้ให้อัตโนมัติ)

**ติดตั้งเอง — shadcn/ui · React Aria**

1. ติดตั้ง dependencies: `npm install clsx tailwind-merge thaizip`
1. เพิ่ม shadcn/ui primitives ที่คอมโพเนนต์ใช้: `npx shadcn@latest add select label button input`
1. คัดลอกไฟล์ต่อไปนี้ลงในโปรเจกต์:
   - `lib/utils.ts` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/lib/utils.ts)
   - `hooks/use-thai-address-index.ts` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-index.ts)
   - `hooks/use-thai-address-cascade.ts` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-cascade.ts)
   - `components/thai-address-cascade-select.tsx` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/shadcn/aria/thai-address-cascade-select.tsx)
1. แก้ import `@/lib/...` และ `@/hooks/...` ให้ตรงกับตำแหน่งไฟล์ในโปรเจกต์ (CLI แก้ให้อัตโนมัติ)

## วิธีใช้

```tsx
import { ThaiAddressCascadeSelect } from '@/components/thai-address-cascade-select'
```

```tsx
<ThaiAddressCascadeSelect onValueChange={(address) => console.log(address)} />
```

`onValueChange` จะส่ง `ResolvedThaiAddress | null` เมื่อเลือกครบทั้งสามระดับ

### เมื่อเปลี่ยนตัวเลือกหลัก

เมื่อเลือกที่อยู่ครบแล้ว การเปลี่ยนจังหวัดหรืออำเภอ/เขตจะเรียก `onValueChange(null)` และล้างตัวเลือกปลายทางที่ไม่สอดคล้องกัน ผู้ใช้จึงต้องเลือกตัวเลือกที่อยู่ถัดไปใหม่

## ตัวอย่าง

### เลือกข้อมูลภาษาอังกฤษ

ใช้ `locale="en"` เพื่อแสดงป้ายและตัวเลือกเป็นภาษาอังกฤษ

```tsx title="cascade-select-en.tsx"
'use client'

import { ThaiAddressCascadeSelect } from '@/components/thai-address-cascade-select'

export default function CascadeSelectEn() {
  return <ThaiAddressCascadeSelect locale="en" onValueChange={(address) => console.log(address)} />
}
```

### ควบคุมค่าจากภายนอก

ส่ง `value` เพื่อควบคุมค่าทั้งชุด หรือ `defaultValue` เพื่อตั้งลำดับที่เลือกไว้ตอนเริ่มต้น ตัวอย่างนี้แสดงค่า callback และมีปุ่มล้างค่า

```tsx title="cascade-select-controlled.tsx"
'use client'

import { useState } from 'react'
import type { ResolvedThaiAddress } from 'thaizip'
import { ThaiAddressCascadeSelect } from '@/components/thai-address-cascade-select'

export default function CascadeSelectControlled() {
  const [address, setAddress] = useState<ResolvedThaiAddress | null>(null)

  return (
    <div className="flex flex-col gap-3">
      <ThaiAddressCascadeSelect value={address} onValueChange={setAddress} />
      <div>
        <button type="button" className="rounded-md border border-input bg-background px-3 py-1.5 text-sm hover:bg-accent" onClick={() => setAddress(null)}>
          ล้างค่า
        </button>
      </div>
      <p className="text-sm font-medium">ค่าปัจจุบัน</p>
      <pre className="max-h-72 overflow-auto rounded-md border bg-muted p-3 text-xs">{JSON.stringify(address, null, 2)}</pre>
    </div>
  )
}
```

### ปิดใช้งาน

ส่ง `disabled` เพื่อปิดทุก select

```tsx title="cascade-select-disabled.tsx"
'use client'

import { ThaiAddressCascadeSelect } from '@/components/thai-address-cascade-select'

export default function CascadeSelectDisabled() {
  return <ThaiAddressCascadeSelect disabled />
}
```

### ไม่ผ่านการตรวจสอบ

ส่ง `aria-invalid` เพื่อทำเครื่องหมายว่าไม่ผ่านการตรวจสอบทั้งสาม trigger

```tsx title="cascade-select-invalid.tsx"
'use client'

import { ThaiAddressCascadeSelect } from '@/components/thai-address-cascade-select'

export default function CascadeSelectInvalid() {
  return <ThaiAddressCascadeSelect aria-invalid />
}
```

### ปรับข้อความ

ใช้ `texts` เพื่อแทนที่ป้ายกำกับและ placeholder เช่นให้ตรงกับคำว่าเขต/แขวงของกรุงเทพฯ คีย์ที่ไม่ได้ส่งจะใช้ค่าเริ่มต้นตาม `locale`

```tsx title="cascade-select-texts.tsx"
import { ThaiAddressCascadeSelect } from '@/components/thai-address-cascade-select'

export default function CascadeSelectTexts() {
  return (
    <ThaiAddressCascadeSelect
      texts={{
        provinceLabel: 'จังหวัดที่จัดส่ง',
        provincePlaceholder: 'เลือกจังหวัด',
        districtLabel: 'เขต/อำเภอ',
        subdistrictLabel: 'แขวง/ตำบล',
      }}
    />
  )
}
```

### ใช้ในฟอร์ม

ส่ง `name` เพื่อส่งค่าที่เลือกไปกับฟอร์มปกติ คอมโพเนนต์จะสร้าง hidden inputs สี่ช่อง (`address-subdistrict`, `address-district`, `address-province`, `address-zipcode`) และ `required` จะกันไม่ให้ส่งฟอร์มจนกว่าจะเลือกครบทั้งสามระดับ ดูรูปแบบอื่นได้ที่ [การใช้งานร่วมกับฟอร์ม](https://react-thai-zip.vercel.app/docs/guides/forms)

```tsx title="form-cascade-select.tsx"
'use client'

import { useRef, useState, type FormEvent } from 'react'
import { ThaiAddressCascadeSelect } from '@/components/thai-address-cascade-select'

export default function FormCascadeSelect() {
  const ref = useRef<HTMLButtonElement>(null)
  const [payload, setPayload] = useState<Record<string, FormDataEntryValue> | null>(null)

  function handleSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault()
    setPayload(Object.fromEntries(new FormData(event.currentTarget)))
  }

  return (
    <form className="flex flex-col gap-3" onSubmit={handleSubmit}>
      <ThaiAddressCascadeSelect
        ref={ref}
        name="address"
        required
      />
      <div className="flex gap-2">
        <button type="button" className="rounded-md border border-input bg-background px-3 py-1.5 text-sm hover:bg-accent" onClick={() => ref.current?.focus()}>
          โฟกัสช่องที่อยู่
        </button>
        <button type="submit" className="rounded-md border border-input bg-background px-3 py-1.5 text-sm hover:bg-accent">
          ส่งฟอร์ม
        </button>
      </div>
      <p className="text-sm font-medium">FormData ที่ส่ง</p>
      <pre className="max-h-72 overflow-auto rounded-md border bg-muted p-3 text-xs">{JSON.stringify(payload, null, 2)}</pre>
    </form>
  )
}
```

## API Reference

| Prop | Type | ค่าเริ่มต้น | รายละเอียด |
| --- | --- | --- | --- |
| `value` | `ResolvedThaiAddress \| null` | — | ค่าที่ควบคุมอยู่; ส่ง `null` เพื่อล้าง |
| `defaultValue` | `ResolvedThaiAddress \| null` | — | ค่าเริ่มต้นของโหมด uncontrolled และเลือกลำดับที่อยู่ให้ครบตั้งแต่เริ่ม |
| `onValueChange` | `(address: ResolvedThaiAddress \| null) => void` | — | เรียกเมื่อเลือกครบหรือเมื่อค่าถูกล้าง |
| `name` | `string` | — | สร้าง hidden inputs สี่ช่อง: `<name>-subdistrict`, `-district`, `-province`, `-zipcode` |
| `locale` | `'th' \| 'en'` | `'th'` | ภาษาของป้ายและตัวเลือก |
| `texts` | `Partial<ThaiAddressCascadeSelectTexts>` | — | แทนที่ข้อความ `*Label`/`*Placeholder` ของแต่ละระดับ รวมถึง `loadingText`, `errorText`, `retryLabel` |
| `indexLoad` | `'mount' \| 'visible'` | `'mount'` | โหลดข้อมูลที่อยู่ตอน mount หรือรอจนช่องเลื่อนเข้ามาในจอ |
| `disabled` | `boolean` | `false` | ปิดใช้งานทุก select |
| `required` | `boolean` | — | กำหนดให้ทั้งสาม select ต้องมีค่า |
| `aria-invalid` | `React.AriaAttributes['aria-invalid']` | — | ทำเครื่องหมายว่าไม่ผ่านการตรวจสอบทั้งสาม trigger |
| `onBlur` | `React.FocusEventHandler<HTMLButtonElement>` | — | ทำงานเมื่อ trigger ของจังหวัดสูญเสียโฟกัส |
| `onError` | `(error: Error) => void` | — | ทำงานเมื่อโหลดดัชนีที่อยู่ไม่สำเร็จ |
| `className` | `string` | — | grid wrapper หลัก |
| `labelClassName` | `string` | — | ป้ายกำกับของทุกช่อง |
| `triggerClassName` | `string` | — | trigger ของ select และช่องรหัสไปรษณีย์ |
| `popupClassName` | `string` | — | กล่องตัวเลือก |
| `itemClassName` | `string` | — | แต่ละตัวเลือก |
| `ref` | `React.Ref<HTMLButtonElement>` | — | ส่งต่อไปยัง trigger ของจังหวัด ซึ่งเป็นตัวควบคุมหลักของ cascade |

ทั้ง `ref` และ `onBlur` ผูกกับ trigger ของจังหวัด ไม่ใช่ trigger ของอำเภอหรือตำบล

## ดูเพิ่มเติม

ดู [การใช้งานร่วมกับฟอร์ม](https://react-thai-zip.vercel.app/docs/guides/forms) สำหรับการส่งค่าที่อยู่ และ [การปรับแต่ง](https://react-thai-zip.vercel.app/docs/guides/customization) สำหรับปรับหน้าตา ส่วนการค้นหาด้วยข้อความ ดู [autocomplete](https://react-thai-zip.vercel.app/docs/components/autocomplete)

---

# ฟอร์มที่อยู่แบบเต็ม

> กรอกบ้านเลขที่ หมู่ ซอย ถนน พร้อมเลือกที่อยู่ตามลำดับชั้นในฟอร์มเดียว

Source: https://react-thai-zip.vercel.app/docs/components/address-form

`ThaiAddressForm` เหมาะกับฟอร์มที่ต้องการที่อยู่แบบเต็ม คือบ้านเลขที่/หมู่/ซอย/ถนน บวกกับตำบล อำเภอ/เขต และจังหวัด โดยฝัง [cascade select](https://react-thai-zip.vercel.app/docs/components/cascade-select) ไว้ภายในให้แล้ว

```tsx title="address-form-demo.tsx"
'use client'

import { ThaiAddressForm } from '@/components/thai-address-form'

export default function AddressFormDemo() {
  return <ThaiAddressForm onValueChange={(address) => console.log(address)} />
}
```

## ติดตั้ง

**CLI**

```bash
npx react-thaizip add address-form
```

CLI เลือกไฟล์ให้ตาม style ที่ `init` บันทึกไว้: โปรเจกต์ที่มี `components.json` ของ shadcn/ui จะได้ไฟล์ที่ประกอบจาก primitives ของ shadcn (Base UI, Radix หรือ React Aria) นอกนั้นได้แบบ vanilla

**ติดตั้งเอง — Vanilla**

1. ติดตั้ง dependencies: `npm install clsx tailwind-merge thaizip @base-ui/react`
1. คัดลอกไฟล์ต่อไปนี้ลงในโปรเจกต์:
   - `lib/utils.ts` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/lib/utils.ts)
   - `hooks/use-thai-address-index.ts` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-index.ts)
   - `hooks/use-thai-address-cascade.ts` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-cascade.ts)
   - `components/thai-address-cascade-select.tsx` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/thai-address-cascade-select.tsx)
   - `components/thai-address-form.tsx` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/thai-address-form.tsx)
1. แก้ import `@/lib/...` และ `@/hooks/...` ให้ตรงกับตำแหน่งไฟล์ในโปรเจกต์ (CLI แก้ให้อัตโนมัติ)
1. แบบ vanilla ต้องมี design tokens ใน global stylesheet ด้วย ดู [การปรับแต่ง](https://react-thai-zip.vercel.app/docs/guides/customization#tokens-ที่-init-เพิ่ม)

**ติดตั้งเอง — shadcn/ui · Base UI**

1. ติดตั้ง dependencies: `npm install clsx tailwind-merge thaizip`
1. เพิ่ม shadcn/ui primitives ที่คอมโพเนนต์ใช้: `npx shadcn@latest add select label button input`
1. คัดลอกไฟล์ต่อไปนี้ลงในโปรเจกต์:
   - `lib/utils.ts` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/lib/utils.ts)
   - `hooks/use-thai-address-index.ts` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-index.ts)
   - `hooks/use-thai-address-cascade.ts` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-cascade.ts)
   - `components/thai-address-cascade-select.tsx` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/shadcn/base/thai-address-cascade-select.tsx)
   - `components/thai-address-form.tsx` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/shadcn/base/thai-address-form.tsx)
1. แก้ import `@/lib/...` และ `@/hooks/...` ให้ตรงกับตำแหน่งไฟล์ในโปรเจกต์ (CLI แก้ให้อัตโนมัติ)

**ติดตั้งเอง — shadcn/ui · Radix**

1. ติดตั้ง dependencies: `npm install clsx tailwind-merge thaizip`
1. เพิ่ม shadcn/ui primitives ที่คอมโพเนนต์ใช้: `npx shadcn@latest add select label button input`
1. คัดลอกไฟล์ต่อไปนี้ลงในโปรเจกต์:
   - `lib/utils.ts` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/lib/utils.ts)
   - `hooks/use-thai-address-index.ts` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-index.ts)
   - `hooks/use-thai-address-cascade.ts` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-cascade.ts)
   - `components/thai-address-cascade-select.tsx` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/shadcn/radix/thai-address-cascade-select.tsx)
   - `components/thai-address-form.tsx` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/shadcn/radix/thai-address-form.tsx)
1. แก้ import `@/lib/...` และ `@/hooks/...` ให้ตรงกับตำแหน่งไฟล์ในโปรเจกต์ (CLI แก้ให้อัตโนมัติ)

**ติดตั้งเอง — shadcn/ui · React Aria**

1. ติดตั้ง dependencies: `npm install clsx tailwind-merge thaizip`
1. เพิ่ม shadcn/ui primitives ที่คอมโพเนนต์ใช้: `npx shadcn@latest add select label button input`
1. คัดลอกไฟล์ต่อไปนี้ลงในโปรเจกต์:
   - `lib/utils.ts` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/lib/utils.ts)
   - `hooks/use-thai-address-index.ts` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-index.ts)
   - `hooks/use-thai-address-cascade.ts` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-cascade.ts)
   - `components/thai-address-cascade-select.tsx` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/shadcn/aria/thai-address-cascade-select.tsx)
   - `components/thai-address-form.tsx` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/shadcn/aria/thai-address-form.tsx)
1. แก้ import `@/lib/...` และ `@/hooks/...` ให้ตรงกับตำแหน่งไฟล์ในโปรเจกต์ (CLI แก้ให้อัตโนมัติ)

## วิธีใช้

```tsx
import { ThaiAddressForm } from '@/components/thai-address-form'
```

```tsx
<ThaiAddressForm onValueChange={(address) => console.log(address)} />
```

ค่าใน `onValueChange` คือ `FullThaiAddress | null` จะไม่เป็น `null` ก็ต่อเมื่อกรอกบ้านเลขที่แล้วและเลือกที่อยู่ตามลำดับชั้นครบทั้งสามระดับ

## ตัวอย่าง

### กรอกข้อมูลภาษาอังกฤษ

ใช้ `locale="en"` เพื่อแสดงป้ายกำกับและตัวเลือกเป็นภาษาอังกฤษ ทั้งฟิลด์ข้อความของตัวเองและ cascade select ที่ฝังอยู่

```tsx title="address-form-en.tsx"
'use client'

import { ThaiAddressForm } from '@/components/thai-address-form'

export default function AddressFormEn() {
  return <ThaiAddressForm locale="en" onValueChange={(address) => console.log(address)} />
}
```

### ควบคุมค่าจากภายนอก

ส่ง `value` เพื่อควบคุมค่าทั้งชุด หรือ `defaultValue` เพื่อตั้งค่าเริ่มต้น การเลือกที่อยู่ตามลำดับชั้นที่ยังเลือกไม่ครบจะไม่ถูกล้าง แม้ `value` ที่ควบคุมอยู่จะเป็น `null`

```tsx title="address-form-controlled.tsx"
'use client'

import { useState } from 'react'
import { ThaiAddressForm, type FullThaiAddress } from '@/components/thai-address-form'

export default function AddressFormControlled() {
  const [address, setAddress] = useState<FullThaiAddress | null>(null)

  return (
    <div className="flex flex-col gap-3">
      <ThaiAddressForm value={address} onValueChange={setAddress} />
      <div>
        <button type="button" className="rounded-md border border-input bg-background px-3 py-1.5 text-sm hover:bg-accent" onClick={() => setAddress(null)}>
          ล้างค่า
        </button>
      </div>
      <p className="text-sm font-medium">ค่าปัจจุบัน</p>
      <pre className="max-h-72 overflow-auto rounded-md border bg-muted p-3 text-xs">{JSON.stringify(address, null, 2)}</pre>
    </div>
  )
}
```

### ปิดใช้งาน

ส่ง `disabled` เพื่อปิดทุกช่องกรอกและทุก select

```tsx title="address-form-disabled.tsx"
'use client'

import { ThaiAddressForm } from '@/components/thai-address-form'

export default function AddressFormDisabled() {
  return <ThaiAddressForm disabled />
}
```

### ไม่ผ่านการตรวจสอบ

ส่ง `aria-invalid` เพื่อทำเครื่องหมายว่าช่องกรอกทั้งสี่และ trigger ของ cascade ไม่ผ่านการตรวจสอบ

```tsx title="address-form-invalid.tsx"
'use client'

import { ThaiAddressForm } from '@/components/thai-address-form'

export default function AddressFormInvalid() {
  return <ThaiAddressForm aria-invalid />
}
```

### ปรับข้อความ

ใช้ `texts` สำหรับป้ายกำกับและ placeholder ของคอมโพเนนต์นี้ และ `cascadeTexts` สำหรับ cascade select ที่ฝังอยู่

```tsx title="address-form-texts.tsx"
import { ThaiAddressForm } from '@/components/thai-address-form'

export default function AddressFormTexts() {
  return (
    <ThaiAddressForm
      texts={{
        houseNoLabel: 'บ้านเลขที่ / ห้อง',
        houseNoPlaceholder: 'เช่น 99/9 ห้อง 1203',
        streetLabel: 'อาคาร / ถนน',
      }}
    />
  )
}
```

### ใช้ในฟอร์ม

ฟอร์มแบบหน้าชำระเงิน: มีช่องชื่อผู้รับและเบอร์โทรคู่กับที่อยู่ พร้อมปุ่มยกเลิกและบันทึก การส่ง `name` จะสร้าง hidden inputs แปดช่อง `FormData` ธรรมดาจึงได้ที่อยู่ครบทั้งชุด ดูรูปแบบอื่นได้ที่ [การใช้งานร่วมกับฟอร์ม](https://react-thai-zip.vercel.app/docs/guides/forms)

```tsx title="address-form-submit.tsx"
'use client'

import { useState, type FormEvent } from 'react'
import { ThaiAddressForm } from '@/components/thai-address-form'

export default function AddressFormSubmit() {
  const [payload, setPayload] = useState<Record<string, FormDataEntryValue> | null>(null)

  function handleSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault()
    setPayload(Object.fromEntries(new FormData(event.currentTarget)))
  }

  return (
    <form className="flex flex-col gap-4" onSubmit={handleSubmit} onReset={() => setPayload(null)}>
      <div className="grid gap-4 sm:grid-cols-2">
        <label className="flex flex-col gap-1.5 text-sm font-medium">
          ชื่อผู้รับ
          <input name="recipient" required className="h-9 rounded-md border border-input bg-background px-3 text-sm" />
        </label>
        <label className="flex flex-col gap-1.5 text-sm font-medium">
          เบอร์โทรศัพท์
          <input name="phone" type="tel" required className="h-9 rounded-md border border-input bg-background px-3 text-sm" />
        </label>
      </div>
      <ThaiAddressForm name="address" required />
      <div className="flex justify-end gap-2">
        <button type="reset" className="rounded-md border border-input bg-background px-3 py-1.5 text-sm hover:bg-accent">
          ยกเลิก
        </button>
        <button type="submit" className="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90">
          บันทึกที่อยู่
        </button>
      </div>
      <p className="text-sm font-medium">FormData ที่ส่ง</p>
      <pre className="max-h-72 overflow-auto rounded-md border bg-muted p-3 text-xs">{JSON.stringify(payload, null, 2)}</pre>
    </form>
  )
}
```

## API Reference

| Prop | Type | ค่าเริ่มต้น | รายละเอียด |
| --- | --- | --- | --- |
| `value` | `FullThaiAddress \| null` | — | ค่าที่ควบคุมอยู่; ส่ง `null` เพื่อล้าง |
| `defaultValue` | `FullThaiAddress \| null` | — | ค่าเริ่มต้นของโหมด uncontrolled เติมทั้งช่องข้อความและ cascade ที่ฝังอยู่ |
| `onValueChange` | `(address: FullThaiAddress \| null) => void` | — | เรียกเมื่อบ้านเลขที่ไม่ว่างและเลือกครบทั้งสามระดับ หรือเมื่อค่ากลับมาไม่ครบอีกครั้ง |
| `name` | `string` | — | สร้าง hidden inputs 8 ช่อง: 4 ช่องของ cascade (`-subdistrict`/`-district`/`-province`/`-zipcode`) และ 4 ช่องของคอมโพเนนต์นี้ (`-houseno`/`-moo`/`-soi`/`-street`) |
| `locale` | `'th' \| 'en'` | `'th'` | ภาษาของข้อความและป้ายกำกับ ทั้งของคอมโพเนนต์นี้และ cascade ที่ฝังอยู่ |
| `texts` | `Partial<ThaiAddressFormTexts>` | — | แทนที่ป้ายกำกับและ placeholder ของบ้านเลขที่/หมู่/ซอย/ถนน |
| `cascadeTexts` | `Partial<ThaiAddressCascadeSelectTexts>` | — | ส่งต่อเป็น `texts` ของ cascade select ที่ฝังอยู่ |
| `indexLoad` | `'mount' \| 'visible'` | `'mount'` | ส่งต่อไปยังตัวโหลดดัชนีที่อยู่ของ cascade ที่ฝังอยู่ |
| `disabled` | `boolean` | `false` | ปิดใช้งานทุกช่องข้อความและ cascade ที่ฝังอยู่ |
| `required` | `boolean` | `false` | กำหนดให้ช่องบ้านเลขที่และ cascade ต้องมีค่า (หมู่/ซอย/ถนนยังไม่บังคับ) |
| `aria-invalid` | `React.AriaAttributes['aria-invalid']` | — | ทำเครื่องหมายว่าช่องข้อความทั้งสี่และ trigger ของ cascade ไม่ผ่านการตรวจสอบ |
| `onBlur` | `React.FocusEventHandler<HTMLInputElement>` | — | ทำงานเฉพาะเมื่อช่องบ้านเลขที่สูญเสียโฟกัส |
| `onError` | `(error: Error) => void` | — | ทำงานเมื่อโหลดดัชนีที่อยู่ไม่สำเร็จ (จัดการโดย cascade ที่ฝังอยู่) |
| `className` | `string` | — | wrapper หลัก |
| `labelClassName` | `string` | — | ป้ายกำกับของช่องข้อความและ cascade |
| `inputClassName` | `string` | — | ช่องบ้านเลขที่/หมู่/ซอย/ถนน |
| `triggerClassName` | `string` | — | trigger ของ select และช่องรหัสไปรษณีย์ใน cascade |
| `popupClassName` | `string` | — | กล่องตัวเลือกของ cascade |
| `itemClassName` | `string` | — | แต่ละตัวเลือกของ cascade |
| `ref` | `React.Ref<HTMLInputElement>` | — | ส่งต่อไปยังช่องบ้านเลขที่ |

## ดูเพิ่มเติม

ดู [การใช้งานร่วมกับฟอร์ม](https://react-thai-zip.vercel.app/docs/guides/forms) สำหรับการส่งค่าที่อยู่ และ [การปรับแต่ง](https://react-thai-zip.vercel.app/docs/guides/customization) สำหรับปรับหน้าตา ส่วนคอมโพเนนต์ที่ฝังอยู่ ดู [cascade select](https://react-thai-zip.vercel.app/docs/components/cascade-select)

---

# แสดงที่อยู่แบบอ่านอย่างเดียว

> จัดรูปแบบที่อยู่ไทยที่เลือกไว้แล้วให้เป็นข้อความอ่านง่าย ไม่ต้องโหลดดัชนีที่อยู่

Source: https://react-thai-zip.vercel.app/docs/components/address-display

`ThaiAddressDisplay` เหมาะกับหน้าที่ต้องแสดงที่อยู่ที่บันทึกไว้แล้ว เช่นหน้าสรุปคำสั่งซื้อหรือโปรไฟล์ผู้ใช้ เป็นคอมโพเนนต์อ่านอย่างเดียว ไม่มี Base UI และไม่โหลดดัชนีที่อยู่ จึงมี style เดียว

```tsx title="address-display-demo.tsx"
import { ThaiAddressDisplay, type ThaiAddressDisplayValue } from '@/components/thai-address-display'

const address: ThaiAddressDisplayValue = {
  tambon: 'คลองตันเหนือ',
  tambonEn: 'Khlong Tan Nuea',
  amphure: 'วัฒนา',
  amphureEn: 'Watthana',
  province: 'กรุงเทพมหานคร',
  provinceEn: 'Bangkok',
  zipCode: '10110',
  subdistrict: 'คลองตันเหนือ',
  subdistrictEn: 'Khlong Tan Nuea',
  district: 'วัฒนา',
  districtEn: 'Watthana',
  postalCode: '10110',
  houseNo: '99/9',
  moo: '4',
  soi: '2',
  street: 'สุขุมวิท',
}

export default function AddressDisplayDemo() {
  return <ThaiAddressDisplay value={address} />
}
```

## ติดตั้ง

**CLI**

```bash
npx react-thaizip add address-display
```

CLI เลือกไฟล์ให้ตาม style ที่ `init` บันทึกไว้: โปรเจกต์ที่มี `components.json` ของ shadcn/ui จะได้ไฟล์ที่ประกอบจาก primitives ของ shadcn (Base UI, Radix หรือ React Aria) นอกนั้นได้แบบ vanilla

**ติดตั้งเอง — Vanilla**

1. ติดตั้ง dependencies: `npm install clsx tailwind-merge thaizip`
1. คัดลอกไฟล์ต่อไปนี้ลงในโปรเจกต์:
   - `lib/utils.ts` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/lib/utils.ts)
   - `components/thai-address-display.tsx` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/thai-address-display.tsx)
1. แก้ import `@/lib/...` และ `@/hooks/...` ให้ตรงกับตำแหน่งไฟล์ในโปรเจกต์ (CLI แก้ให้อัตโนมัติ)
1. แบบ vanilla ต้องมี design tokens ใน global stylesheet ด้วย ดู [การปรับแต่ง](https://react-thai-zip.vercel.app/docs/guides/customization#tokens-ที่-init-เพิ่ม)

## วิธีใช้

```tsx
import { ThaiAddressDisplay } from '@/components/thai-address-display'
```

```tsx
<ThaiAddressDisplay value={address} />
```

`value` รับที่อยู่ที่ resolve ไว้แล้ว (`ResolvedThaiAddress` บวก `houseNo`/`moo`/`soi`/`street` แบบ optional มี type ชื่อ `ThaiAddressDisplayValue`) หรือ `null` เมื่อยังไม่มีที่อยู่ ซึ่งจะแสดง `emptyText` แทน

## ตัวอย่าง

### หลายบรรทัด

ใช้ `mode="multi-line"` เพื่อแยกส่วนบ้านเลขที่/หมู่/ซอย/ถนน กับส่วนตำบล-อำเภอ-จังหวัดออกเป็นคนละบรรทัด

```tsx title="address-display-multi-line.tsx"
import { ThaiAddressDisplay, type ThaiAddressDisplayValue } from '@/components/thai-address-display'

const address: ThaiAddressDisplayValue = {
  tambon: 'คลองตันเหนือ',
  tambonEn: 'Khlong Tan Nuea',
  amphure: 'วัฒนา',
  amphureEn: 'Watthana',
  province: 'กรุงเทพมหานคร',
  provinceEn: 'Bangkok',
  zipCode: '10110',
  subdistrict: 'คลองตันเหนือ',
  subdistrictEn: 'Khlong Tan Nuea',
  district: 'วัฒนา',
  districtEn: 'Watthana',
  postalCode: '10110',
  houseNo: '99/9',
  moo: '4',
  soi: '2',
  street: 'สุขุมวิท',
}

export default function AddressDisplayMultiLine() {
  return <ThaiAddressDisplay value={address} mode="multi-line" />
}
```

### แสดงเป็นภาษาอังกฤษ

ใช้ `locale="en"` เพื่อแสดงชื่อภาษาอังกฤษ คำว่า "หมู่"/"ซอย" จะเปลี่ยนเป็นคำทับศัพท์มาตรฐาน "Moo"/"Soi" โดยอัตโนมัติ ส่วนบ้านเลขที่และชื่อถนนแสดงตามที่กรอกไว้เสมอ

```tsx title="address-display-en.tsx"
import { ThaiAddressDisplay, type ThaiAddressDisplayValue } from '@/components/thai-address-display'

const address: ThaiAddressDisplayValue = {
  tambon: 'คลองตันเหนือ',
  tambonEn: 'Khlong Tan Nuea',
  amphure: 'วัฒนา',
  amphureEn: 'Watthana',
  province: 'กรุงเทพมหานคร',
  provinceEn: 'Bangkok',
  zipCode: '10110',
  subdistrict: 'คลองตันเหนือ',
  subdistrictEn: 'Khlong Tan Nuea',
  district: 'วัฒนา',
  districtEn: 'Watthana',
  postalCode: '10110',
  houseNo: '99/9',
  moo: '4',
  soi: '2',
  street: 'สุขุมวิท',
}

export default function AddressDisplayEn() {
  return <ThaiAddressDisplay value={address} locale="en" />
}
```

### ไม่มีบ้านเลขที่

เมื่อ `value` ไม่มี `houseNo`/`moo`/`soi`/`street` เลย จะแสดงเฉพาะตำบล-อำเภอ-จังหวัด

```tsx title="address-display-minimal.tsx"
import { ThaiAddressDisplay, type ThaiAddressDisplayValue } from '@/components/thai-address-display'

const address: ThaiAddressDisplayValue = {
  tambon: 'คลองตันเหนือ',
  tambonEn: 'Khlong Tan Nuea',
  amphure: 'วัฒนา',
  amphureEn: 'Watthana',
  province: 'กรุงเทพมหานคร',
  provinceEn: 'Bangkok',
  zipCode: '10110',
  subdistrict: 'คลองตันเหนือ',
  subdistrictEn: 'Khlong Tan Nuea',
  district: 'วัฒนา',
  districtEn: 'Watthana',
  postalCode: '10110',
}

export default function AddressDisplayMinimal() {
  return <ThaiAddressDisplay value={address} />
}
```

### ยังไม่มีที่อยู่

ส่ง `value={null}` เพื่อแสดง `emptyText` แทน (ค่าเริ่มต้นคือ "ไม่มีที่อยู่" หรือ "No address" ตาม `locale`)

```tsx title="address-display-empty.tsx"
import { ThaiAddressDisplay } from '@/components/thai-address-display'

export default function AddressDisplayEmpty() {
  return <ThaiAddressDisplay value={null} />
}
```

### ปรับสไตล์

ใช้ `className` กับ `<address>` และ `lineClassName` กับแต่ละบรรทัดในโหมดหลายบรรทัด ตัวอย่างนี้ทำเป็นการ์ดและเน้นบรรทัดแรก

```tsx title="address-display-styled.tsx"
import { ThaiAddressDisplay, type ThaiAddressDisplayValue } from '@/components/thai-address-display'

const address: ThaiAddressDisplayValue = {
  tambon: 'คลองตันเหนือ',
  tambonEn: 'Khlong Tan Nuea',
  amphure: 'วัฒนา',
  amphureEn: 'Watthana',
  province: 'กรุงเทพมหานคร',
  provinceEn: 'Bangkok',
  zipCode: '10110',
  subdistrict: 'คลองตันเหนือ',
  subdistrictEn: 'Khlong Tan Nuea',
  district: 'วัฒนา',
  districtEn: 'Watthana',
  postalCode: '10110',
  houseNo: '99/9',
  moo: '4',
  soi: '2',
  street: 'สุขุมวิท',
}

export default function AddressDisplayStyled() {
  return (
    <ThaiAddressDisplay
      value={address}
      mode="multi-line"
      className="rounded-lg border p-4 not-italic"
      lineClassName="first:font-medium"
    />
  )
}
```

## API Reference

| Prop | Type | ค่าเริ่มต้น | รายละเอียด |
| --- | --- | --- | --- |
| `value` | `ThaiAddressDisplayValue \| null` | — | **จำเป็น** ที่อยู่ที่จะแสดง; `null` จะแสดง `emptyText` แทน |
| `locale` | `'th' \| 'en'` | `'th'` | กำหนดภาษาของชื่อพื้นที่และ `emptyText` เริ่มต้น |
| `mode` | `'single-line' \| 'multi-line'` | `'single-line'` | `'single-line'` ต่อส่วนถนนกับพื้นที่ด้วย `', '` ส่วน `'multi-line'` แสดงเป็นสองบรรทัด |
| `emptyText` | `string` | `'ไม่มีที่อยู่'` / `'No address'` | ข้อความเมื่อ `value` เป็น `null` ตาม `locale` |
| `className` | `string` | — | `<address>` หลัก |
| `lineClassName` | `string` | — | แต่ละบรรทัดในโหมด `'multi-line'` |
| `ref` | `React.Ref<HTMLElement>` | — | ส่งต่อไปยัง `<address>` |

## ดูเพิ่มเติม

ดู [การปรับแต่ง](https://react-thai-zip.vercel.app/docs/guides/customization) สำหรับปรับหน้าตา ถ้าต้องการให้ผู้ใช้เลือกหรือแก้ไขที่อยู่ ดู [ฟอร์มที่อยู่แบบเต็ม](https://react-thai-zip.vercel.app/docs/components/address-form) หรือ [cascade select](https://react-thai-zip.vercel.app/docs/components/cascade-select)

---

# ฟิลด์ที่อยู่สำหรับ react-hook-form

> ครอบ cascade select ด้วย Controller ของ react-hook-form เพื่อผูกกับ validation และ state ของฟอร์มที่มีอยู่แล้ว

Source: https://react-thai-zip.vercel.app/docs/components/address-form-field

`ThaiAddressFormField` เหมาะกับฟอร์มที่ใช้ [react-hook-form](https://react-hook-form.com/) อยู่แล้วและต้องการผูกที่อยู่เข้ากับ state ของฟอร์มโดยตรง แทนที่จะอ่านค่าจาก hidden inputs รูปแบบ prop ต่างจากคอมโพเนนต์อื่นโดยตั้งใจ: ส่ง `control`/`name`/`rules` เหมือนกับ `useController` ของ react-hook-form เอง และไม่มี `value`/`onValueChange`/hidden inputs

```tsx title="address-form-field-demo.tsx"
'use client'

import { useState } from 'react'
import { useForm } from 'react-hook-form'
import type { ResolvedThaiAddress } from 'thaizip'
import { ThaiAddressFormField } from '@/components/thai-address-form-field'

type FormValues = {
  address: ResolvedThaiAddress | null
}

export default function AddressFormFieldDemo() {
  const { control, handleSubmit } = useForm<FormValues>({ defaultValues: { address: null } })
  const [submitted, setSubmitted] = useState<ResolvedThaiAddress | null>(null)

  return (
    <form className="flex flex-col gap-3" onSubmit={handleSubmit((data) => setSubmitted(data.address))}>
      <ThaiAddressFormField
        control={control}
        name="address"
      />
      <div>
        <button type="submit" className="rounded-md border border-input bg-background px-3 py-1.5 text-sm hover:bg-accent">
          ส่งฟอร์ม
        </button>
      </div>
      <p className="text-sm font-medium">ค่าที่ส่ง</p>
      <pre className="max-h-72 overflow-auto rounded-md border bg-muted p-3 text-xs">{JSON.stringify(submitted, null, 2)}</pre>
    </form>
  )
}
```

## ติดตั้ง

**CLI**

```bash
npx react-thaizip add address-form-field
```

CLI เลือกไฟล์ให้ตาม style ที่ `init` บันทึกไว้: โปรเจกต์ที่มี `components.json` ของ shadcn/ui จะได้ไฟล์ที่ประกอบจาก primitives ของ shadcn (Base UI, Radix หรือ React Aria) นอกนั้นได้แบบ vanilla

**ติดตั้งเอง — Vanilla**

1. ติดตั้ง dependencies: `npm install clsx tailwind-merge thaizip @base-ui/react react-hook-form`
1. คัดลอกไฟล์ต่อไปนี้ลงในโปรเจกต์:
   - `lib/utils.ts` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/lib/utils.ts)
   - `hooks/use-thai-address-index.ts` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-index.ts)
   - `hooks/use-thai-address-cascade.ts` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-cascade.ts)
   - `components/thai-address-cascade-select.tsx` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/thai-address-cascade-select.tsx)
   - `components/thai-address-form-field.tsx` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/thai-address-form-field.tsx)
1. แก้ import `@/lib/...` และ `@/hooks/...` ให้ตรงกับตำแหน่งไฟล์ในโปรเจกต์ (CLI แก้ให้อัตโนมัติ)
1. แบบ vanilla ต้องมี design tokens ใน global stylesheet ด้วย ดู [การปรับแต่ง](https://react-thai-zip.vercel.app/docs/guides/customization#tokens-ที่-init-เพิ่ม)

**ติดตั้งเอง — shadcn/ui · Base UI**

1. ติดตั้ง dependencies: `npm install clsx tailwind-merge thaizip react-hook-form`
1. เพิ่ม shadcn/ui primitives ที่คอมโพเนนต์ใช้: `npx shadcn@latest add select label button input`
1. คัดลอกไฟล์ต่อไปนี้ลงในโปรเจกต์:
   - `lib/utils.ts` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/lib/utils.ts)
   - `hooks/use-thai-address-index.ts` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-index.ts)
   - `hooks/use-thai-address-cascade.ts` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-cascade.ts)
   - `components/thai-address-cascade-select.tsx` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/shadcn/base/thai-address-cascade-select.tsx)
   - `components/thai-address-form-field.tsx` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/shadcn/base/thai-address-form-field.tsx)
1. แก้ import `@/lib/...` และ `@/hooks/...` ให้ตรงกับตำแหน่งไฟล์ในโปรเจกต์ (CLI แก้ให้อัตโนมัติ)

**ติดตั้งเอง — shadcn/ui · Radix**

1. ติดตั้ง dependencies: `npm install clsx tailwind-merge thaizip react-hook-form`
1. เพิ่ม shadcn/ui primitives ที่คอมโพเนนต์ใช้: `npx shadcn@latest add select label button input`
1. คัดลอกไฟล์ต่อไปนี้ลงในโปรเจกต์:
   - `lib/utils.ts` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/lib/utils.ts)
   - `hooks/use-thai-address-index.ts` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-index.ts)
   - `hooks/use-thai-address-cascade.ts` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-cascade.ts)
   - `components/thai-address-cascade-select.tsx` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/shadcn/radix/thai-address-cascade-select.tsx)
   - `components/thai-address-form-field.tsx` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/shadcn/radix/thai-address-form-field.tsx)
1. แก้ import `@/lib/...` และ `@/hooks/...` ให้ตรงกับตำแหน่งไฟล์ในโปรเจกต์ (CLI แก้ให้อัตโนมัติ)

**ติดตั้งเอง — shadcn/ui · React Aria**

1. ติดตั้ง dependencies: `npm install clsx tailwind-merge thaizip react-hook-form`
1. เพิ่ม shadcn/ui primitives ที่คอมโพเนนต์ใช้: `npx shadcn@latest add select label button input`
1. คัดลอกไฟล์ต่อไปนี้ลงในโปรเจกต์:
   - `lib/utils.ts` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/lib/utils.ts)
   - `hooks/use-thai-address-index.ts` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-index.ts)
   - `hooks/use-thai-address-cascade.ts` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-cascade.ts)
   - `components/thai-address-cascade-select.tsx` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/shadcn/aria/thai-address-cascade-select.tsx)
   - `components/thai-address-form-field.tsx` — [ซอร์ส](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/shadcn/aria/thai-address-form-field.tsx)
1. แก้ import `@/lib/...` และ `@/hooks/...` ให้ตรงกับตำแหน่งไฟล์ในโปรเจกต์ (CLI แก้ให้อัตโนมัติ)

## วิธีใช้

```tsx
import { useForm } from 'react-hook-form'
import type { ResolvedThaiAddress } from 'thaizip'
import { ThaiAddressFormField } from '@/components/thai-address-form-field'
```

```tsx
const { control, handleSubmit } = useForm<{ address: ResolvedThaiAddress | null }>({
  defaultValues: { address: null },
})
```

```tsx
<ThaiAddressFormField control={control} name="address" />
```

ฟิลด์ `name` ที่ระบุ (`"address"` ในตัวอย่าง) ต้องมีชนิดค่าเป็น `ResolvedThaiAddress | null` ใน type ของฟอร์ม `handleSubmit` ของ react-hook-form อ่านค่าได้โดยตรง คอมโพเนนต์นี้ไม่ render hidden inputs

## ตัวอย่าง

### กรอกข้อมูลภาษาอังกฤษ

ใช้ `locale="en"` เพื่อแสดงป้ายและตัวเลือกของ cascade select ที่ฝังอยู่เป็นภาษาอังกฤษ

```tsx title="address-form-field-en.tsx"
'use client'

import { useState } from 'react'
import { useForm } from 'react-hook-form'
import type { ResolvedThaiAddress } from 'thaizip'
import { ThaiAddressFormField } from '@/components/thai-address-form-field'

type FormValues = {
  address: ResolvedThaiAddress | null
}

export default function AddressFormFieldEn() {
  const { control, handleSubmit } = useForm<FormValues>({ defaultValues: { address: null } })
  const [submitted, setSubmitted] = useState<ResolvedThaiAddress | null>(null)

  return (
    <form className="flex flex-col gap-3" onSubmit={handleSubmit((data) => setSubmitted(data.address))}>
      <ThaiAddressFormField
        control={control}
        name="address"
        locale="en"
      />
      <div>
        <button type="submit" className="rounded-md border border-input bg-background px-3 py-1.5 text-sm hover:bg-accent">
          Submit
        </button>
      </div>
      <p className="text-sm font-medium">Submitted value</p>
      <pre className="max-h-72 overflow-auto rounded-md border bg-muted p-3 text-xs">{JSON.stringify(submitted, null, 2)}</pre>
    </form>
  )
}
```

### ตรวจสอบด้วย validation rule

ส่ง `rules` แบบเดียวกับที่ส่งให้ `Controller`/`useController` ของ react-hook-form ตัวอย่างนี้กำหนดให้ต้องเลือกที่อยู่ครบก่อนส่งฟอร์ม ข้อความ error จะแสดงเป็น `<p role="alert">` ใต้ cascade select

```tsx title="address-form-field-validation.tsx"
'use client'

import { useState } from 'react'
import { useForm } from 'react-hook-form'
import type { ResolvedThaiAddress } from 'thaizip'
import { ThaiAddressFormField } from '@/components/thai-address-form-field'

type FormValues = {
  address: ResolvedThaiAddress | null
}

export default function AddressFormFieldValidation() {
  const { control, handleSubmit } = useForm<FormValues>({ defaultValues: { address: null } })
  const [submitted, setSubmitted] = useState<ResolvedThaiAddress | null>(null)

  return (
    <form className="flex flex-col gap-3" onSubmit={handleSubmit((data) => setSubmitted(data.address))}>
      <ThaiAddressFormField
        control={control}
        name="address"
        rules={{ validate: (value) => value !== null || 'กรุณาเลือกที่อยู่ให้ครบ' }}
      />
      <div>
        <button type="submit" className="rounded-md border border-input bg-background px-3 py-1.5 text-sm hover:bg-accent">
          ส่งฟอร์ม
        </button>
      </div>
      <p className="text-sm font-medium">ค่าที่ส่ง</p>
      <pre className="max-h-72 overflow-auto rounded-md border bg-muted p-3 text-xs">{JSON.stringify(submitted, null, 2)}</pre>
    </form>
  )
}
```

### ปิดใช้งาน

ส่ง `disabled` เพื่อปิดทุก select ใน cascade select ที่ฝังอยู่

```tsx title="address-form-field-disabled.tsx"
'use client'

import { useForm } from 'react-hook-form'
import type { ResolvedThaiAddress } from 'thaizip'
import { ThaiAddressFormField } from '@/components/thai-address-form-field'

type FormValues = {
  address: ResolvedThaiAddress | null
}

export default function AddressFormFieldDisabled() {
  const { control } = useForm<FormValues>({ defaultValues: { address: null } })

  return <ThaiAddressFormField control={control} name="address" disabled />
}
```

### ใช้ในฟอร์ม

ฟิลด์ที่อยู่คู่กับ input ทั่วไปที่ใช้ `register` ในฟอร์มเดียวกัน ทั้งสองช่องตรวจสอบตอนส่ง และ `handleSubmit` ได้ค่าทั้งหมดพร้อมกัน

```tsx title="address-form-field-full.tsx"
'use client'

import { useState } from 'react'
import { useForm } from 'react-hook-form'
import type { ResolvedThaiAddress } from 'thaizip'
import { ThaiAddressFormField } from '@/components/thai-address-form-field'

type FormValues = {
  name: string
  address: ResolvedThaiAddress | null
}

export default function AddressFormFieldFull() {
  const { control, register, handleSubmit, formState: { errors } } = useForm<FormValues>({
    defaultValues: { name: '', address: null },
  })
  const [submitted, setSubmitted] = useState<FormValues | null>(null)

  return (
    <form className="flex flex-col gap-4" onSubmit={handleSubmit(setSubmitted)}>
      <label className="flex flex-col gap-1.5 text-sm font-medium">
        ชื่อ-นามสกุล
        <input
          {...register('name', { required: 'กรุณากรอกชื่อ' })}
          aria-invalid={errors.name ? true : undefined}
          className="h-9 rounded-md border border-input bg-background px-3 text-sm"
        />
        {errors.name && <span role="alert" className="text-sm font-normal text-destructive">{errors.name.message}</span>}
      </label>
      <ThaiAddressFormField
        control={control}
        name="address"
        rules={{ validate: (value) => value !== null || 'กรุณาเลือกที่อยู่ให้ครบ' }}
      />
      <div>
        <button type="submit" className="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90">
          สมัครสมาชิก
        </button>
      </div>
      <p className="text-sm font-medium">ค่าที่ส่ง</p>
      <pre className="max-h-72 overflow-auto rounded-md border bg-muted p-3 text-xs">{JSON.stringify(submitted, null, 2)}</pre>
    </form>
  )
}
```

## API Reference

| Prop | Type | ค่าเริ่มต้น | รายละเอียด |
| --- | --- | --- | --- |
| `control` | `Control<TFieldValues>` | — | **จำเป็น** ค่า `control` จาก `useForm()` ของ react-hook-form |
| `name` | `FieldPath<TFieldValues>` | — | **จำเป็น** path ของฟิลด์ในฟอร์ม ชนิดค่าต้องเป็น `ResolvedThaiAddress \| null` |
| `rules` | `Omit<RegisterOptions<…>, 'valueAsNumber' \| 'valueAsDate' \| 'setValueAs' \| 'disabled'>` | — | ส่งต่อไปยัง `Controller` ของ react-hook-form เพื่อตรวจสอบค่า |
| `locale` | `'th' \| 'en'` | `'th'` | ภาษาของป้ายและตัวเลือกใน cascade select ที่ฝังอยู่ |
| `texts` | `Partial<ThaiAddressCascadeSelectTexts>` | — | ส่งต่อเป็น `texts` ของ cascade select ที่ฝังอยู่ |
| `indexLoad` | `'mount' \| 'visible'` | `'mount'` | ส่งต่อไปยังตัวโหลดดัชนีที่อยู่ของ cascade ที่ฝังอยู่ |
| `disabled` | `boolean` | — | ปิดใช้งานทุก select ใน cascade select ที่ฝังอยู่ |
| `className` | `string` | — | `<div>` ที่ครอบ cascade select และข้อความ validation |
| `labelClassName` | `string` | — | ป้ายกำกับของ cascade select |
| `triggerClassName` | `string` | — | trigger และช่องรหัสไปรษณีย์ของ cascade select |
| `popupClassName` | `string` | — | กล่องตัวเลือกของ cascade select |
| `itemClassName` | `string` | — | แต่ละตัวเลือกของ cascade select |
| `errorClassName` | `string` | — | ข้อความ validation `role="alert"` ที่แสดงเมื่อตรวจสอบไม่ผ่าน |

คอมโพเนนต์นี้ไม่รับ `value`/`onValueChange`/`ref`/`name` (สำหรับ hidden input) แบบคอมโพเนนต์อื่น react-hook-form อ่านและเขียนค่าผ่าน `control`/`name` ที่ส่งให้แทน

## ดูเพิ่มเติม

ดู [การปรับแต่ง](https://react-thai-zip.vercel.app/docs/guides/customization) สำหรับปรับหน้าตา ถ้าฟอร์มไม่ได้ใช้ react-hook-form ดู [cascade select](https://react-thai-zip.vercel.app/docs/components/cascade-select) หรือ [ฟอร์มที่อยู่แบบเต็ม](https://react-thai-zip.vercel.app/docs/components/address-form) ซึ่งใช้รูปแบบ `value`/`onValueChange`/hidden input ตามปกติ

---

# การใช้งานร่วมกับฟอร์ม

> ส่งค่าที่อยู่ไทยด้วยฟอร์มของเบราว์เซอร์ หรือเชื่อมต่อกับ state ของไลบรารีฟอร์ม

Source: https://react-thai-zip.vercel.app/docs/guides/forms

คอมโพเนนต์ที่ CLI สร้างเป็น element ของฟอร์มในเบราว์เซอร์ปกติ ไม่ได้ส่งข้อมูลหรือเก็บข้อมูลให้คุณเอง เมื่อใส่ `name` คอมโพเนนต์จะสร้าง hidden inputs เพื่อให้ `FormData` และการ submit ตามปกติอ่านค่าที่อยู่ได้

## ส่งด้วยฟอร์มของเบราว์เซอร์

ตั้ง `name="address"` เพื่อให้ได้ฟิลด์สี่ช่องนี้:

```text
address-subdistrict
address-district
address-province
address-zipcode
```

หลังเลือกที่อยู่แล้ว `new FormData(form)` มี payload รูปแบบนี้ โดยชื่อพื้นที่เป็นชื่อภาษาไทยของข้อมูลที่เลือก แม้ UI จะแสดงภาษาอังกฤษ:

```ts
{
  'address-subdistrict': 'คลองตันเหนือ',
  'address-district': 'วัฒนา',
  'address-province': 'กรุงเทพมหานคร',
  'address-zipcode': '10110',
}
```

ตัวอย่างนี้ดัก `submit` ไว้ อ่าน `FormData` และแสดงผลในหน้านี้ จึงใช้งานได้โดยไม่ต้องมี server:

```tsx title="form-autocomplete.tsx"
'use client'

import { useRef, useState, type FormEvent } from 'react'
import { ThaiAddressAutocomplete } from '@/components/thai-address-autocomplete'

export default function FormAutocomplete() {
  const ref = useRef<HTMLInputElement>(null)
  const [payload, setPayload] = useState<Record<string, FormDataEntryValue> | null>(null)

  function handleSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault()
    setPayload(Object.fromEntries(new FormData(event.currentTarget)))
  }

  return (
    <form className="flex flex-col gap-3" onSubmit={handleSubmit}>
      <ThaiAddressAutocomplete
        ref={ref}
        name="address"
        required
      />
      <div className="flex gap-2">
        <button type="button" className="rounded-md border border-input bg-background px-3 py-1.5 text-sm hover:bg-accent" onClick={() => ref.current?.focus()}>
          โฟกัสช่องที่อยู่
        </button>
        <button type="submit" className="rounded-md border border-input bg-background px-3 py-1.5 text-sm hover:bg-accent">
          ส่งฟอร์ม
        </button>
      </div>
      <p className="text-sm font-medium">FormData ที่ส่ง</p>
      <pre className="max-h-72 overflow-auto rounded-md border bg-muted p-3 text-xs">{JSON.stringify(payload, null, 2)}</pre>
    </form>
  )
}
```

```tsx title="form-cascade-select.tsx"
'use client'

import { useRef, useState, type FormEvent } from 'react'
import { ThaiAddressCascadeSelect } from '@/components/thai-address-cascade-select'

export default function FormCascadeSelect() {
  const ref = useRef<HTMLButtonElement>(null)
  const [payload, setPayload] = useState<Record<string, FormDataEntryValue> | null>(null)

  function handleSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault()
    setPayload(Object.fromEntries(new FormData(event.currentTarget)))
  }

  return (
    <form className="flex flex-col gap-3" onSubmit={handleSubmit}>
      <ThaiAddressCascadeSelect
        ref={ref}
        name="address"
        required
      />
      <div className="flex gap-2">
        <button type="button" className="rounded-md border border-input bg-background px-3 py-1.5 text-sm hover:bg-accent" onClick={() => ref.current?.focus()}>
          โฟกัสช่องที่อยู่
        </button>
        <button type="submit" className="rounded-md border border-input bg-background px-3 py-1.5 text-sm hover:bg-accent">
          ส่งฟอร์ม
        </button>
      </div>
      <p className="text-sm font-medium">FormData ที่ส่ง</p>
      <pre className="max-h-72 overflow-auto rounded-md border bg-muted p-3 text-xs">{JSON.stringify(payload, null, 2)}</pre>
    </form>
  )
}
```

ทั้ง autocomplete และ cascade select สร้างฟิลด์ชื่อเดียวกัน หากส่ง `disabled` ควบคู่กัน hidden inputs ทั้งสี่ช่องก็จะถูก disabled และจะไม่ปรากฏใน `FormData`

## ใช้กับไลบรารีฟอร์ม

เก็บ `ResolvedThaiAddress | null` ไว้ใน state ของฟอร์มหรือ controller ของไลบรารีที่ใช้ แล้วส่งกลับเข้าคอมโพเนนต์ผ่าน `value` สำหรับ autocomplete การจัดการแบบ controlled ทำให้ validation, reset และการ submit ของฟอร์มมีแหล่งข้อมูลเดียวกัน

```tsx
const [address, setAddress] = useState<ResolvedThaiAddress | null>(null)

<ThaiAddressAutocomplete
  name="address"
  value={address}
  onValueChange={setAddress}
/>
```

สำหรับ `ThaiAddressCascadeSelect` ให้ใช้ค่าในฟอร์มกับ resolved address ที่เลือกครบเท่านั้น cascade select จัดการการเลือกจังหวัด/อำเภอที่ยังไม่ครบภายใน และจะไม่ส่ง resolved value จนกว่าจะเลือกครบทั้งสามระดับ การส่ง `value={null}` จะล้าง selection ที่เลือกครบอยู่ แต่ตั้งใจคงการเลือกบางส่วนที่กำลังดำเนินอยู่ไว้ ดังนั้น state ของฟอร์มจึงไม่ใช่แหล่งข้อมูลเดียวของ UI การเลือกบางส่วนนี้

เมื่อ reset ฟอร์มตามปกติ ให้ส่ง `null` เพื่อล้างที่อยู่ที่เลือกครบซึ่งเก็บไว้ หาก reset ต้องล้างการเลือกบางส่วนของ cascade ด้วย ให้ remount cascade โดยตั้งใจด้วยการเพิ่มค่า reset key:

```tsx
<ThaiAddressCascadeSelect key={resetVersion} name="address" value={address} onValueChange={setAddress} />
```

## การตรวจสอบและการเข้าถึง

- `required` ส่งต่อไปยัง input ของ autocomplete หรือ select ทั้งสามของ cascade select
- `aria-invalid` ระบุสถานะไม่ผ่าน validation: autocomplete ส่งให้ input และ cascade select ส่งให้ trigger ทั้งสาม
- `onBlur` ของ autocomplete ทำงานเมื่อ input สูญเสียโฟกัส ส่วนของ cascade select ทำงานเมื่อ province trigger สูญเสียโฟกัส
- `ref` ของ autocomplete ชี้ไปที่ `HTMLInputElement`; `ref` ของ cascade select ชี้ไปที่ปุ่ม province trigger จึงใช้สำหรับ focus จาก logic ของฟอร์มได้

ตัวอย่างเช่น ส่ง `aria-invalid={Boolean(error)}` ตาม validation state ของฟอร์ม และเรียก `ref.current?.focus()` เพื่อพาผู้ใช้กลับไปยัง control หลักที่ต้องแก้ไข

## คู่มือที่เกี่ยวข้อง

ดู [autocomplete](https://react-thai-zip.vercel.app/docs/components/autocomplete) สำหรับ UI ค้นหาจากข้อความ, [cascade select](https://react-thai-zip.vercel.app/docs/components/cascade-select) สำหรับการเลือกตามลำดับ และ [การปรับแต่ง](https://react-thai-zip.vercel.app/docs/guides/customization) สำหรับข้อความและ class slots

---

# การปรับแต่ง

> เปลี่ยนภาษา ข้อความ class slots และ design tokens ของคอมโพเนนต์ที่ scaffold

Source: https://react-thai-zip.vercel.app/docs/guides/customization

ไฟล์ที่ `react-thaizip add` สร้างเป็นซอร์สในโปรเจกต์ของคุณ จึงเริ่มปรับแต่งด้วย props และ Tailwind classes ก่อน แล้วแก้ซอร์สที่ scaffold เมื่อจำเป็นต้องเปลี่ยนพฤติกรรมหรือ markup ให้ลึกกว่านั้น

## ภาษาและข้อความ

ส่ง `locale="en"` เพื่อใช้ label, placeholder และตัวเลือกภาษาอังกฤษ `texts` รับค่าเพียงบางส่วนได้ โดยคีย์ที่ไม่ได้ระบุจะใช้ข้อความมาตรฐานของ locale นั้น

```tsx
<ThaiAddressAutocomplete
  locale="en"
  texts={{
    placeholder: 'Search delivery address',
    emptyText: 'No delivery area found',
  }}
/>
```

autocomplete รองรับ `placeholder`, `clearAriaLabel`, `loadingText`, `errorText`, `retryLabel` และ `emptyText` ใน `texts` ส่วน cascade select รองรับ `provinceLabel`, `districtLabel`, `subdistrictLabel`, `zipLabel`, placeholder ทั้งสามช่อง และ `loadingText`, `errorText`, `retryLabel`

## Class slots ของ autocomplete

| Prop | ใช้กับ |
| --- | --- |
| `className` | wrapper หลัก |
| `inputClassName` | ช่องค้นหา |
| `popupClassName` | กล่องผลลัพธ์ |
| `itemClassName` | แต่ละผลลัพธ์ |

```tsx
<ThaiAddressAutocomplete
  inputClassName="border-sky-500 focus-visible:ring-sky-500"
  popupClassName="border-sky-200 shadow-lg"
/>
```

## Class slots ของ cascade select

| Prop | ใช้กับ |
| --- | --- |
| `className` | grid wrapper หลัก |
| `labelClassName` | ป้ายกำกับทุกฟิลด์ |
| `triggerClassName` | select trigger และช่องรหัสไปรษณีย์ |
| `popupClassName` | กล่องตัวเลือก |
| `itemClassName` | แต่ละตัวเลือก |

ส่ง `triggerClassName` เมื่อต้องการให้ trigger ทั้งสามและช่องรหัสไปรษณีย์มีหน้าตาเดียวกัน หรือใช้ `labelClassName` เพื่อจัด label ทั้งชุด

## Tokens ที่ `init` เพิ่ม

เมื่อ `init` เพิ่ม token block ลงใน global stylesheet จะใช้ CSS custom properties ต่อไปนี้กับคอมโพเนนต์ เช่น `--background`, `--foreground`, `--popover`, `--primary`, `--muted`, `--accent`, `--destructive`, `--border`, `--input`, `--ring` และ `--radius`

ตัวอย่างจาก block ของ Tailwind v4:

```css
:root {
  --background: oklch(1 0 0);
  --foreground: oklch(0.145 0 0);
  --popover: oklch(1 0 0);
  --primary: oklch(0.205 0 0);
  --muted: oklch(0.97 0 0);
  --accent: oklch(0.97 0 0);
  --destructive: oklch(0.577 0.245 27.325);
  --border: oklch(0.922 0 0);
  --input: oklch(0.922 0 0);
  --ring: oklch(0.708 0 0);
  --radius: 0.625rem;
}
```

ใน block เดียวกัน `.dark` จะ override color properties เหล่านี้ ส่วน `--radius` ยังใช้ค่าร่วมจาก `:root` เว้นแต่จะ override เพิ่มเอง แก้ค่าของ theme ที่ต้องการใน stylesheet เพื่อเปลี่ยนสีของ component ที่ scaffold

## Tailwind v4

สำหรับ Tailwind v4 `init` เพิ่ม `@theme inline` ใน token block ที่เขียนลง stylesheet โดย map CSS properties ไปเป็น Tailwind theme values อัตโนมัติ:

```css
@theme inline {
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  --color-popover: var(--popover);
  --color-primary: var(--primary);
  --color-muted: var(--muted);
  --color-accent: var(--accent);
  --color-destructive: var(--destructive);
  --color-border: var(--border);
  --color-input: var(--input);
  --color-ring: var(--ring);
  --radius-lg: var(--radius);
  --radius-md: calc(var(--radius) - 2px);
  --radius-sm: calc(var(--radius) - 4px);
}
```

จึงใช้ classes เช่น `bg-background`, `border-input` และ `focus-visible:ring-ring` ได้ทันทีหลัง `init` เขียน block นี้

## Tailwind v3

Tailwind v3 ไม่มี `@theme inline` ดังนั้น `init` จะพิมพ์ snippet นี้ให้เพิ่มเองภายใต้ `theme.extend` ใน `tailwind.config.{js,ts}`:

```ts
colors: {
  background: 'hsl(var(--background))',
  foreground: 'hsl(var(--foreground))',
  popover: { DEFAULT: 'hsl(var(--popover))', foreground: 'hsl(var(--popover-foreground))' },
  primary: { DEFAULT: 'hsl(var(--primary))', foreground: 'hsl(var(--primary-foreground))' },
  muted: { DEFAULT: 'hsl(var(--muted))', foreground: 'hsl(var(--muted-foreground))' },
  accent: { DEFAULT: 'hsl(var(--accent))', foreground: 'hsl(var(--accent-foreground))' },
  destructive: { DEFAULT: 'hsl(var(--destructive))', foreground: 'hsl(var(--destructive-foreground))' },
  border: 'hsl(var(--border))',
  input: 'hsl(var(--input))',
  ring: 'hsl(var(--ring))',
},
borderRadius: { lg: 'var(--radius)', md: 'calc(var(--radius) - 2px)', sm: 'calc(var(--radius) - 4px)' },
```

ขั้นตอนนี้จำเป็นสำหรับ Tailwind v3 ก่อนที่ token classes ของคอมโพเนนต์จะ resolve ได้ หาก classes ยังไม่มี style ให้ตรวจตาม [การแก้ปัญหา Tailwind v3](https://react-thai-zip.vercel.app/docs/troubleshooting#tailwind-v3-ไม่มี-styles-ของ-token)

## ปรับลึกกว่าด้วยซอร์สที่ scaffold

หากต้องการเปลี่ยน layout, เพิ่ม slot หรือเปลี่ยนพฤติกรรม ให้แก้ไฟล์ component ที่ `add` สร้างในโปรเจกต์ของคุณโดยตรง คง import ของ `utils` และ hook ที่ CLI สร้างไว้ แล้วปรับ markup และ Tailwind classes ให้เข้ากับ design system ของคุณ

## คู่มือที่เกี่ยวข้อง

ดู [การใช้งานร่วมกับฟอร์ม](https://react-thai-zip.vercel.app/docs/guides/forms) สำหรับ `name`, hidden fields และ validation semantics

---

# ใช้ react-thaizip กับ AI

> ให้ AI scaffold และประกอบคอมโพเนนต์ที่อยู่ไทยจากซอร์สจริงในโปรเจกต์

Source: https://react-thai-zip.vercel.app/docs/guides/using-with-ai

ให้ AI ใช้ CLI เพื่อเพิ่มคอมโพเนนต์ลงในโปรเจกต์ React หรือ Next.js แล้วเขียนหน้าฟอร์มโดย import ไฟล์ที่ได้ คอมโพเนนต์เป็นซอร์สในโปรเจกต์ของคุณ ไม่ใช่ runtime export จากแพ็กเกจ `react-thaizip`

## เลือกคอมโพเนนต์

| งานที่ต้องการ | คำสั่ง `add` | คอมโพเนนต์ |
| --- | --- | --- |
| ค้นหาที่อยู่จากข้อความหรือรหัสไปรษณีย์ | `autocomplete` | `ThaiAddressAutocomplete` |
| เลือกจังหวัด → อำเภอ/เขต → ตำบล/แขวง | `cascade-select` | `ThaiAddressCascadeSelect` |
| กรอกบ้านเลขที่และรายละเอียดถนนด้วย | `address-form` | `ThaiAddressForm` |
| แสดงที่อยู่ที่บันทึกไว้ | `address-display` | `ThaiAddressDisplay` |
| ใช้ cascade กับ react-hook-form | `address-form-field` | `ThaiAddressFormField` |

อ่านพฤติกรรมและ props ของแต่ละตัวในหมวด Components ก่อนเลือก โดยเฉพาะรูปแบบ `ResolvedThaiAddress | null`, hidden inputs ของฟอร์ม และการ reset cascade ที่ยังเลือกไม่ครบ

## ขั้นตอนในโปรเจกต์ปลายทาง

รันจากรากโปรเจกต์ React หรือ Next.js ที่ติดตั้ง Tailwind CSS v3 หรือ v4 แล้ว:

```bash
npx react-thaizip init
npx react-thaizip add autocomplete
```

`init` สร้าง `thaizip.config.json` v4 และเลือก template แบบ vanilla หรือ shadcn/ui ตาม `components.json` ของโปรเจกต์ ส่วน `add` ติดตั้ง dependencies และพิมพ์ import path ของไฟล์ที่สร้าง ใช้ path ที่พิมพ์จริง เพราะตำแหน่งไฟล์และนามสกุล `.tsx`/`.jsx` ขึ้นอยู่กับโปรเจกต์ หลีกเลี่ยง `--overwrite` หากยังไม่ได้ตรวจซอร์สที่เคยแก้เอง

## Prompt สำหรับ AI

คัดลอกข้อความนี้ไปให้ coding agent ที่เข้าถึงโปรเจกต์ปลายทางได้ แล้วแทนงานในวงเล็บตามต้องการ:

```text
สร้าง [หน้ากรอกที่อยู่สำหรับ checkout] ในโปรเจกต์นี้ด้วย react-thaizip

1. ตรวจ package.json, Tailwind, components.json และ thaizip.config.json ก่อน
2. ถ้ายังไม่ได้ตั้งค่า ให้รัน npx react-thaizip init จากรากโปรเจกต์
3. เลือกคอมโพเนนต์ที่เหมาะ แล้วรัน npx react-thaizip add <target>
4. ใช้ import path ที่ CLI พิมพ์ออกมา และ import คอมโพเนนต์จากไฟล์ที่ scaffold ในโปรเจกต์
5. ใช้ API ตาม docs ของคอมโพเนนต์นั้น; เมื่อเป็นฟอร์ม ให้จัดการค่า null, validation, submit และ reset
6. อย่าเขียน implementation ของคอมโพเนนต์ที่ CLI สร้างขึ้นใหม่ และอย่าเขียนทับไฟล์เดิมโดยไม่ตรวจ diff
7. รัน typecheck/build ของโปรเจกต์ แล้วสรุปไฟล์ที่สร้างและผลการตรวจ
```

## Markdown สำหรับ AI

ทุกหน้าในเอกสารมีฉบับ Markdown ที่รวมซอร์สโค้ดของทุกตัวอย่างและขั้นตอนติดตั้งเองไว้แล้ว ส่งให้ agent ที่อ่านเว็บได้พร้อมคำสั่งงาน:

| URL | เนื้อหา |
| --- | --- |
| [`/llms.txt`](https://react-thai-zip.vercel.app/llms.txt) | สารบัญทุกหน้า พร้อมลิงก์ไปยังไฟล์ `.md` |
| [`/llms-full.txt`](https://react-thai-zip.vercel.app/llms-full.txt) | เนื้อหาทุกหน้าในไฟล์เดียว |
| [`/docs.md`](https://react-thai-zip.vercel.app/docs.md) | หน้าใดก็ได้: เติม `.md` ท้าย URL เช่น [`/docs/components/autocomplete.md`](https://react-thai-zip.vercel.app/docs/components/autocomplete.md) |
| [thaizip `/llms.txt`](https://naay99999.github.io/thai-zip/llms.txt) | API ค้นหาที่อยู่ระดับล่างที่คอมโพเนนต์ใช้ |

แต่ละหน้ายังมีปุ่ม Copy Markdown ด้านบนสำหรับคัดลอกเฉพาะหน้านั้นด้วย

## ตัวอย่างใช้ไฟล์ที่สร้าง

ตัวอย่างนี้ใช้ alias `@/components/...` เหมือนหน้าอื่นในเอกสาร หากโปรเจกต์ไม่มี alias นี้หรือใช้ path อื่น ให้ใช้ import path ที่ `add` พิมพ์แทน:

```tsx
'use client'

import { useState } from 'react'
import type { ResolvedThaiAddress } from 'thaizip'
import { ThaiAddressAutocomplete } from '@/components/thai-address-autocomplete'

export function AddressField() {
  const [address, setAddress] = useState<ResolvedThaiAddress | null>(null)

  return (
    <ThaiAddressAutocomplete
      name="address"
      value={address}
      onValueChange={setAddress}
    />
  )
}
```

`onValueChange` ส่ง `null` เมื่อค่าที่เลือกถูกล้าง หากใช้ `name="address"` คอมโพเนนต์จะสร้าง hidden inputs สำหรับ `address-subdistrict`, `address-district`, `address-province` และ `address-zipcode` ดูตัวอย่าง submit ได้ที่ [การใช้งานร่วมกับฟอร์ม](https://react-thai-zip.vercel.app/docs/guides/forms)

## ตรวจงานที่ AI สร้าง

- ไฟล์คอมโพเนนต์มาจาก `add` และ import ชี้ไปยัง path จริงในโปรเจกต์
- `thaizip` ที่ติดตั้งมีเวอร์ชันอย่างน้อย `0.7.5`; dependency และ shadcn primitives ที่ CLI ต้องการติดตั้งครบ
- ฟอร์มรับมือค่า `null`, validation และการล้างค่า; ตรวจชื่อ hidden inputs หรือค่าใน react-hook-form ตามวิธี submit ที่เลือก
- รัน typecheck/build และทดลองเลือกที่อยู่จริงอย่างน้อยหนึ่งรายการ

หากคำสั่งล้มเหลว ดู [การแก้ปัญหา](https://react-thai-zip.vercel.app/docs/troubleshooting), [ข้อมูลอ้างอิง CLI](https://react-thai-zip.vercel.app/docs/reference/cli) และ [การตั้งค่า](https://react-thai-zip.vercel.app/docs/reference/config)

---

# คำสั่ง CLI

> รูปแบบคำสั่ง flags, aliases และการเลือกคอมโพเนนต์ของ react-thaizip

Source: https://react-thai-zip.vercel.app/docs/reference/cli

เรียก CLI ผ่าน `npx react-thaizip` ภายในโฟลเดอร์รากของโปรเจกต์ React หรือ Next.js ที่ต้องการ scaffold

## รูปแบบคำสั่ง

```text
react-thaizip init [--yes]
react-thaizip add [component...] [--yes] [--overwrite]
react-thaizip --help
react-thaizip --version
react-thaizip init --help
react-thaizip add --help
```

เมื่อใช้งานจริง ให้เติม `npx` ข้างหน้า เช่น `npx react-thaizip add autocomplete`

## `init`

```npm
npx react-thaizip init
```

`init` ทำงานตามลำดับนี้:

1. ตรวจหา package manager, โครงสร้างโปรเจกต์, TypeScript, สไตล์ shadcn/ui และ Tailwind CSS v3 หรือ v4 (ต้องติดตั้งไว้ก่อนแล้ว) แล้วพิมพ์สรุปผลตรวจก่อนถามคำถามใด ๆ
2. ให้ยืนยันหรือแก้ `componentDir` ถ้าไม่ได้ส่ง `--yes`
3. เพิ่ม design tokens ลง global CSS ถ้าเจอไฟล์
4. เสนอให้ติดตั้ง `thaizip` ถ้ายังไม่มี
5. สร้าง `thaizip.config.json`
6. ถ้ามีขั้นตอนที่ต้องทำเอง (พิมพ์ `theme.extend` snippet สำหรับ Tailwind v3 หรือ design-token block เมื่อไม่พบ global CSS) จะรวมพิมพ์ทั้งหมดไว้ท้ายสุดใต้หัวข้อ `=== Manual steps required ===`
7. พิมพ์คำแนะนำขั้นต่อไป: `Next: run \`npx react-thaizip add autocomplete\` to add your first component.`

ตัวอย่างผลลัพธ์ตอนเริ่มรัน:

```text
Detected project settings:
  Components directory: app/components
  Lib directory: lib
  Hooks directory: hooks
  Package manager: npm
  Language: TypeScript
  Tailwind: v4 (app/globals.css)
  Component style: vanilla Tailwind (no components.json found)
```

Tailwind จะถูกตรวจก่อนถามเรื่อง `componentDir` เสมอ ดังนั้นโปรเจกต์ที่ยังไม่ได้ติดตั้ง Tailwind จะหยุดทำงานทันทีโดยไม่ถามคำถามใด ๆ ก่อน

ดูรูปแบบไฟล์และกฎการตรวจหาได้ที่ [ข้อมูลอ้างอิงการตั้งค่า](https://react-thai-zip.vercel.app/docs/reference/config)

## `add`

เพิ่มคอมโพเนนต์หนึ่งตัว:

```npm
npx react-thaizip add autocomplete
```

เพิ่มหลายตัวพร้อมกันได้โดยคั่นชื่อด้วยช่องว่าง:

```npm
npx react-thaizip add autocomplete cascade-select
```

ถ้าไม่ระบุชื่อ `add` จะแสดง interactive multiselect ที่มีคอมโพเนนต์สาธารณะทั้งห้าตัว:

```npm
npx react-thaizip add
```

### ชื่อและ aliases ที่ใช้ได้

การจับคู่ชื่อไม่แยกตัวพิมพ์เล็ก-ใหญ่

| คอมโพเนนต์ | ชื่อและ aliases |
| --- | --- |
| `ThaiAddressAutocomplete` | `autocomplete`, `thai-address-autocomplete`, `ThaiAddressAutocomplete` |
| `ThaiAddressCascadeSelect` | `cascade`, `cascade-select`, `thai-address-cascade-select`, `ThaiAddressCascadeSelect` |
| `ThaiAddressForm` | `address-form`, `thai-address-form`, `ThaiAddressForm` |
| `ThaiAddressDisplay` | `address-display`, `thai-address-display`, `ThaiAddressDisplay` |
| `ThaiAddressFormField` | `address-form-field`, `thai-address-form-field`, `ThaiAddressFormField` |

`add` ดึงไฟล์ช่วยเหลือที่แต่ละ target ต้องใช้โดยอัตโนมัติ: ทุกคอมโพเนนต์ใช้ `utils`; ตัวที่ใช้ดัชนีที่อยู่จะดึง index hook และตัวที่ใช้ cascade จะดึง cascade hook และ select ด้วย รายการภายในไม่แสดงใน multiselect หรือ `--help` และไม่ต้องระบุชื่อเอง

ก่อนเขียนไฟล์ `add` จะตรวจว่า `thaizip` ที่มีอยู่เป็นเวอร์ชัน `0.7.5` ขึ้นไป ติดตั้ง dependencies ที่ขาดเมื่อได้รับการยืนยัน แล้วเขียนไฟล์ตามตำแหน่งใน `thaizip.config.json` สไตล์คอมโพเนนต์มาจาก `init`: แบบ vanilla interactive ใช้ Base UI ส่วนโปรเจกต์ shadcn/ui ที่รองรับจะใช้ primitives ของ Base, Radix หรือ React Aria และให้ shadcn CLI ติดตั้ง primitives ที่ขาด

## Flags

| Flag | ผลลัพธ์ |
| --- | --- |
| `--yes`, `-y` | ไม่แสดง confirmation prompts และใช้คำตอบเริ่มต้นของแต่ละ prompt |
| `--overwrite` | เขียนทับไฟล์คอมโพเนนต์ที่มีอยู่โดยไม่ถาม และเปิดให้เขียนทับไฟล์ `lib`/hook ที่ปกติได้รับการป้องกันไว้ ใช้กับ `add` เท่านั้น |
| `--help`, `-h` | แสดง help; วางหลัง `init` หรือ `add` เพื่อดู help เฉพาะคำสั่ง |
| `--version`, `-v` | แสดงเวอร์ชันของ CLI |

Flags ที่รู้จักวางตำแหน่งใดก็ได้ในอาร์กิวเมนต์ ถ้าส่งทั้ง help และ version **help จะทำงานก่อนเสมอ** และจะไม่เรียก `init` หรือ `add`

`--yes` ไม่ได้แปลว่าเขียนทับทุกอย่าง: คำตอบเริ่มต้นของ prompt เขียนทับไฟล์คอมโพเนนต์และไฟล์ config คือ “ไม่” ดังนั้นไฟล์ที่มีอยู่ยังถูกข้าม ใช้ `--overwrite` แยกต่างหากเมื่อตั้งใจแทนที่คอมโพเนนต์ ไฟล์ hook และ `lib` ที่มีอยู่ยังได้รับการป้องกันตามค่าเริ่มต้นเสมอ แม้ส่ง `--yes` เพราะผู้ใช้มักแก้ไฟล์เหล่านี้เอง ต้องส่ง `--overwrite` อย่างชัดเจนเท่านั้นจึงจะเขียนทับได้

ถ้าใช้ flag ที่ไม่รู้จัก CLI จะจบด้วย error ก่อนเรียกคำสั่ง ดูวิธีแก้ปัญหาการติดตั้งและไฟล์ที่มีอยู่ได้ที่ [การแก้ปัญหา](https://react-thai-zip.vercel.app/docs/troubleshooting)

---

# การตั้งค่า

> รูปแบบ thaizip.config.json v4, สไตล์คอมโพเนนต์ และกฎการตรวจหาโปรเจกต์

Source: https://react-thai-zip.vercel.app/docs/reference/config

`npx react-thaizip init` สร้าง `thaizip.config.json` ที่รากโปรเจกต์ คำสั่ง `add` อ่านไฟล์นี้เพื่อหาตำแหน่งเขียนคอมโพเนนต์และไฟล์ช่วยเหลือ

## รูปแบบ v4

ตัวอย่างนี้แสดงทุกฟิลด์ของ config v4 สำหรับโปรเจกต์ที่ไม่มี shadcn/ui แบบที่รองรับ path และเวอร์ชัน CLI ที่บันทึกจริงขึ้นอยู่กับโปรเจกต์

```json
{
  "typescript": true,
  "componentDir": "app/components",
  "libDir": "lib",
  "hooksDir": "hooks",
  "packageManager": "npm",
  "tailwind": {
    "version": 4,
    "css": "app/globals.css"
  },
  "style": "vanilla",
  "shadcnBase": "",
  "shadcnUiAlias": "",
  "shadcnUiDir": "",
  "registryVersion": "0.3.7"
}
```

| ฟิลด์ | ค่าที่ใช้ได้ | ความหมาย |
| --- | --- | --- |
| `typescript` | `true` หรือ `false` | ตรวจพบอัตโนมัติตอน `init` จากการมีไฟล์ `tsconfig.json` (ไม่มีการถามผู้ใช้) กำหนดว่า `add` จะเขียนไฟล์เป็น `.tsx`/`.ts` ตามต้นฉบับ (`true`) หรือตัด syntax ของ TypeScript ออกแล้วเขียนเป็น `.jsx`/`.js` แทน (`false`) |
| `componentDir` | string ที่ไม่ว่าง | ตำแหน่งเขียนไฟล์คอมโพเนนต์ เทียบจากรากโปรเจกต์ |
| `libDir` | string ที่ไม่ว่าง | ตำแหน่งเขียน `utils.ts`; ค่าเริ่มต้นจาก `init` คือ `lib` |
| `hooksDir` | string ที่ไม่ว่าง | ตำแหน่งเขียน shared hooks; ค่าเริ่มต้นคือ `hooks` |
| `packageManager` | `npm`, `yarn`, `pnpm`, `bun` | package manager ที่ `add` ใช้ติดตั้ง dependencies |
| `tailwind.version` | `3` หรือ `4` | major version ของ Tailwind ที่ตรวจพบ |
| `tailwind.css` | string | path ของ global CSS เทียบจากรากโปรเจกต์ หรือ `""` เมื่อหาไฟล์ไม่พบ |
| `style` | `vanilla` หรือ `shadcn` | เลือก template แบบ Tailwind/Base UI หรือแบบที่ประกอบจาก shadcn/ui primitives ของโปรเจกต์; `init` ตรวจหาอัตโนมัติ |
| `shadcnBase` | `""`, `base`, `radix` หรือ `aria` | เป็น `""` เมื่อใช้ `vanilla`; กรณี shadcn ระบุไลบรารีของ UI primitives |
| `shadcnUiAlias` | string | import prefix จาก `components.json` `aliases.ui`; เป็น `""` เมื่อใช้ `vanilla` |
| `shadcnUiDir` | string | directory เทียบจากรากโปรเจกต์ที่ resolve จาก alias; เป็น `""` เมื่อใช้ `vanilla` |
| `registryVersion` | string ที่ไม่ว่าง | เวอร์ชัน CLI ที่บันทึกตอนเขียนไฟล์ครั้งล่าสุด; เป็น metadata ของ registry ไม่ใช่เวอร์ชัน `thaizip` |

Path ทุกค่าถูกตีความจาก working directory ที่เรียก CLI จึงควรรัน `init` และ `add` ที่รากโปรเจกต์เดียวกับไฟล์ config

`registryVersion` ไม่ใช่แค่ metadata เฉย ๆ: `add` ใช้ค่านี้ตรวจความเก่าของไฟล์ `lib`/hook ที่ถูกป้องกันไว้ ถ้า `add` ข้ามไฟล์เหล่านั้นเพราะมีอยู่แล้ว (protected) และ `registryVersion` ที่บันทึกไว้เก่ากว่าเวอร์ชันของ CLI ที่กำลังรัน จะพิมพ์คำเตือนว่าไฟล์นั้นอาจล้าสมัย พร้อมคำสั่งสำหรับ refresh เช่น `npx react-thaizip add utils --overwrite` ทุกครั้งที่ `add` เขียนไฟล์จริงอย่างน้อยหนึ่งไฟล์ (ไม่ว่าจะเป็น component, `lib` หรือ hook) `registryVersion` ใน `thaizip.config.json` จะถูกอัปเดตเป็นเวอร์ชันของ CLI ที่รันอยู่โดยอัตโนมัติ

## การตรวจหาสไตล์คอมโพเนนต์

`init` อ่าน `components.json` ถ้ามี ค่า `style` ที่ขึ้นต้นด้วย `base-`, `radix-` หรือ `aria-` จะเลือก template shadcn/ui ของไลบรารีนั้น ส่วนค่าเก่า `default` และ `new-york` จะเลือก Radix ถ้าไม่มีหรือไม่รู้จัก style จะเลือก `vanilla` สำหรับ shadcn/ui ที่รองรับ `add` จะติดตั้ง UI primitives ที่ยังขาดผ่าน shadcn CLI ส่วนคอมโพเนนต์ interactive แบบ vanilla ใช้ Base UI โดยตรง `ThaiAddressDisplay` ไม่ต้องใช้ UI primitives

ค่า `style` และ `shadcnBase` ถูกบันทึกใน config หากต้องการเปลี่ยนหลัง `init` ให้แก้ฟิลด์ที่เกี่ยวข้องให้สอดคล้องกัน แล้วตรวจไฟล์ก่อนรัน `add --overwrite` เพราะคำสั่งจะเขียนทับซอร์สเดิม

## การตรวจหาโครงสร้างโปรเจกต์

`init` เลือก `componentDir` เริ่มต้นตามลำดับนี้ โดยตรวจจากรากโปรเจกต์:

| เงื่อนไข | Component directory |
| --- | --- |
| มี `app/` | `app/components/` |
| ไม่มี `app/` แต่มี `pages/` | `components/` |
| ไม่มีทั้งสองอย่าง | `src/components/` |

ถ้ามีทั้ง `app/` และ `pages/` จะเลือก `app/components/` เมื่อไม่ใช้ `--yes` คุณแก้ตำแหน่งที่ prompt ได้ก่อนเขียน config

## การตรวจหา package manager

CLI ใช้ lockfile แรกที่พบตามลำดับนี้:

1. `bun.lock` หรือ `bun.lockb` → `bun`
2. `pnpm-lock.yaml` → `pnpm`
3. `yarn.lock` → `yarn`
4. `package-lock.json` → `npm`
5. ถ้าไม่มี lockfile เหล่านี้ → `npm`

## การตรวจหา Tailwind

Tailwind เป็นสิ่งที่ต้องติดตั้งก่อน `init` CLI ไม่ได้ติดตั้ง Tailwind ให้ กฎการตรวจหาทำงานตามลำดับ:

1. หา `@import "tailwindcss"` หรือรูปแบบ single quote ใน global CSS ที่รู้จัก ถ้าพบให้เป็น Tailwind v4 พร้อม path ของไฟล์นั้น
2. ถ้า dependency `tailwindcss` ระบุ major version 4 ขึ้นไป ให้เป็น v4 และเก็บ `tailwind.css` เป็น `""` เมื่อยังหา stylesheet ไม่พบ
3. หา `tailwind.config.ts`, `.js`, `.cjs` หรือ `.mjs` ถ้าพบให้เป็น v3 และหา global CSS ที่มี `@tailwind`
4. ถ้า dependency ระบุ major version 3 ให้เป็น v3 และเก็บ CSS path เป็น `""` เมื่อหาไม่พบ
5. ถ้าไม่มีเงื่อนไขใดตรง `init` จะหยุดและแนะนำให้ติดตั้ง Tailwind

Global CSS ที่ค้นหาตามลำดับคือ `app/globals.css`, `src/app/globals.css`, `styles/globals.css`, `src/styles/globals.css`, `src/index.css`, `src/App.css` และ `app/global.css`

## การเขียนไฟล์และ import paths

Template คอมโพเนนต์ถูกเขียนต้นฉบับด้วย imports `@/lib/*` และ `@/hooks/*` เพื่อ typecheck ใน registry แต่ทุกครั้งที่ `add` คัดลอกไฟล์คอมโพเนนต์ รวมถึงการเขียนทับ จะเขียน specifiers เหล่านั้นเป็น relative paths จาก `componentDir` ไปยัง `libDir` และ `hooksDir` ที่กำหนดใน config โดยไม่ใส่นามสกุลไฟล์

ตัวอย่าง เมื่อ `componentDir` เป็น `app/components`, `libDir` เป็น `lib` และ `hooksDir` เป็น `hooks` imports จะชี้ออกจาก `app/components` ไปยัง directory สองแห่งนั้น ไม่จำเป็นต้องตั้ง alias `@` ในโปรเจกต์ปลายทาง

ไฟล์ `lib` และ hook ที่มีอยู่ได้รับการป้องกันตามค่าเริ่มต้นเสมอ แม้ส่ง `--yes` และจะถูกเขียนทับเฉพาะเมื่อส่ง `--overwrite` อย่างชัดเจนเท่านั้น ส่วนไฟล์คอมโพเนนต์ที่มีอยู่จะถูกข้ามตามค่าเริ่มต้นและจะเขียนทับเฉพาะเมื่อยืนยันหรือส่ง `--overwrite` การ rewrite imports จึงเกิดเฉพาะเมื่อคัดลอกคอมโพเนนต์จริง

## Config รุ่นเก่า

เมื่อ `add` อ่าน config v1, v2 หรือ v3 ที่รองรับได้ จะย้ายเป็น v4 ในตำแหน่งเดิม การย้ายจาก v1 เติม `libDir`, `hooksDir` และ Tailwind metadata (ใช้ Tailwind v4 กับ CSS path ว่างหากตรวจไม่พบ) และเก็บ `typescript: false` ไว้ถ้ามี v1/v2 จะได้ `style: "vanilla"`; v3 ที่เป็น `shadcn` จะได้ `shadcnBase: "base"` เพราะ v3 รองรับเฉพาะ shadcn ที่ใช้ Base UI การย้ายไฟล์ไม่ตรวจหาสไตล์ shadcn ปัจจุบันใหม่ หาก config ที่ได้ไม่ถูกต้อง ให้รัน `npx react-thaizip init` เพื่อสร้างใหม่

หาก path หรือ metadata ไม่ตรงกับโปรเจกต์ ดูขั้นตอนกู้คืนที่ [การแก้ปัญหา](https://react-thai-zip.vercel.app/docs/troubleshooting)

---

# การแก้ปัญหา

> สาเหตุและคำสั่งกู้คืนสำหรับ init, add, Tailwind, dependencies และไฟล์ที่มีอยู่

Source: https://react-thai-zip.vercel.app/docs/troubleshooting

รันคำสั่งทั้งหมดจากรากโปรเจกต์ที่มี `package.json` และ `thaizip.config.json` เว้นแต่หัวข้อนั้นกำลังสร้าง config ใหม่

## `init` ตรวจไม่พบ Tailwind

**ปัญหา:** `init` แสดงว่า Tailwind CSS จำเป็นและหยุดโดยไม่สร้าง config

**สาเหตุ:** โปรเจกต์ไม่มีรูปแบบ Tailwind v3/v4 ที่ CLI รู้จัก Tailwind เป็น prerequisite และ react-thaizip ไม่ติดตั้งให้

**วิธีแก้:** ติดตั้งและตั้งค่า Tailwind ตาม framework ให้มี dependency และรูปแบบ CSS/config ที่ถูกต้อง แล้วรัน:

```npm
npx react-thaizip init
```

## ไม่พบ global CSS

**ปัญหา:** `init` พิมพ์ `No global CSS file was found` ตามด้วย design-token block

**สาเหตุ:** ตรวจพบเวอร์ชัน Tailwind จาก dependency หรือ config แต่ไม่พบ stylesheet ที่รองรับ

**วิธีแก้:** คัดลอก block ทั้งหมดที่ `init` พิมพ์ไปไว้ใน global stylesheet ที่แอปโหลดจริง แล้วเก็บ path ที่ถูกต้องใน `tailwind.css` ของ [`thaizip.config.json`](https://react-thai-zip.vercel.app/docs/reference/config) ถ้าจำเป็น

## Tailwind v3 ไม่มี styles ของ token

**ปัญหา:** คอมโพเนนต์มี classes เช่น `bg-background` หรือ `border-input` แต่ไม่มีสีหรือ style ตามที่คาด

**สาเหตุ:** Tailwind v3 ไม่มี `@theme inline`; `init` จึงพิมพ์ mapping สำหรับ `theme.extend` แต่ไม่แก้ Tailwind config ให้อัตโนมัติ

**วิธีแก้:** นำ `theme.extend` snippet ที่ `init` พิมพ์ไปเพิ่มใต้ `theme.extend` ใน `tailwind.config.js` หรือ `tailwind.config.ts` แล้ว restart dev server ถ้ากำลังทำงานอยู่

## `thaizip` ต่ำกว่า `0.7.5`

**ปัญหา:** `add` รายงานเวอร์ชันที่พบและต้องการ `thaizip >=0.7.5` แล้วหยุดก่อน scaffold

**สาเหตุ:** Template ใช้ cascade/enumeration API และป้ายกำกับสองภาษาที่เริ่มใช้ใน `0.7.0` รวมถึง API อ่าน index cache แบบ synchronous ที่มีตั้งแต่ `0.7.5`

**วิธีแก้:** อัปเกรดแล้วรันคำสั่ง `add` เดิมอีกครั้ง:

```npm
npm i thaizip@latest
npx react-thaizip add autocomplete
```

ถ้าใช้ yarn, pnpm หรือ bun ให้ใช้คำสั่ง add ของ package manager นั้นแทน และเปลี่ยน target ในบรรทัดที่สองให้ตรงกับคำสั่งเดิม

## ติดตั้ง dependencies ไม่สำเร็จ

**ปัญหา:** `init` หรือ `add` แสดง `Failed to install` และรายการแพ็กเกจ

**สาเหตุ:** package manager จบด้วย error เช่น registry, network หรือ lockfile มีปัญหา CLI จะหยุดโดยไม่เขียนคอมโพเนนต์

**วิธีแก้:** ติดตั้งรายการที่ error พิมพ์ด้วย package manager ของโปรเจกต์ แล้วรันคำสั่งเดิมอีกครั้ง ตัวอย่างสำหรับรายการเต็มของ autocomplete แบบ vanilla:

```npm
npm i 'thaizip@^0.7.5' @base-ui/react clsx tailwind-merge
npx react-thaizip add autocomplete
```

ถ้า `init` ล้มเหลวตอนติดตั้ง `thaizip` ให้ติดตั้งแพ็กเกจนั้นก่อน แล้วรัน `npx react-thaizip init` ใหม่

ในโปรเจกต์ shadcn/ui ที่รองรับ `add` ใช้ primitives ของโปรเจกต์แทนการติดตั้ง `@base-ui/react` โดยตรง ให้ทำตามคำสั่งติดตั้ง dependency หรือ shadcn CLI ที่แสดงในรอบที่ล้มเหลว

## มีไฟล์คอมโพเนนต์อยู่แล้ว

**ปัญหา:** `add` ถามว่าจะ overwrite หรือแสดงว่า skipped เพราะไฟล์มีอยู่

**สาเหตุ:** ค่าเริ่มต้นคือเก็บซอร์สคอมโพเนนต์ของคุณไว้ `--yes` ใช้คำตอบเริ่มต้น “ไม่” จึงไม่เขียนทับ

**วิธีแก้:** ตรวจ diff หรือสำรองการแก้ไขของคุณก่อน จากนั้นตอบยืนยันใน prompt หรือใช้ flag นี้เมื่อเจตนาแทนที่ไฟล์จริง:

```npm
npx react-thaizip add autocomplete --overwrite
```

## มีไฟล์ hook หรือ `lib` อยู่แล้ว

**ปัญหา:** `add` แสดง `Skipped lib/utils.ts (protected; pass --overwrite to update it).` หรือข้อความเดียวกันสำหรับ `hooks/use-thai-address-index.ts` แม้ส่ง `--yes`

**สาเหตุ:** เป็นพฤติกรรมตั้งใจเพื่อรักษา shared files ที่คุณอาจแก้ไขเอง ไฟล์เหล่านี้ได้รับการป้องกันตามค่าเริ่มต้นเสมอ แม้ใช้ `--yes` และต่างจากไฟล์คอมโพเนนต์ตรงที่ไม่มี prompt ให้ยืนยัน ต้องส่ง `--overwrite` อย่างชัดเจนเท่านั้นจึงจะเขียนทับได้

**วิธีแก้:** ถ้าไฟล์ที่มีอยู่ทำงานถูกต้อง ไม่ต้องทำอะไร ถ้าต้องการสำเนาใหม่ ให้สำรองการแก้ไขของคุณไว้ก่อน (เพราะจะถูกเขียนทับโดยไม่มีการถาม) แล้วรันคำสั่งเดิมพร้อม `--overwrite`:

```npm
npx react-thaizip add utils --overwrite
```

เปลี่ยน `utils` เป็น `use-thai-address-index` หรือชื่อคอมโพเนนต์ที่ดึงไฟล์นั้นมาเป็น dependency ตามไฟล์ที่ต้องการ refresh

## hook หรือไฟล์ `lib` ที่ scaffold ไว้ดูล้าสมัยหลังอัปเกรด CLI

**ปัญหา:** หลังอัปเกรด `react-thaizip` แล้วรัน `add` อีกครั้ง คำสั่งข้ามไฟล์ `lib/utils.ts` หรือ hook พร้อมข้อความเพิ่มเติมทำนอง `lib/utils.ts predates the current registry (recorded v0.0.1, current v0.3.1) and may be stale. Run \`npx react-thaizip add utils --overwrite\` to refresh it.`

**สาเหตุ:** `thaizip.config.json` เก็บ `registryVersion` ไว้จากครั้งล่าสุดที่ `add` เขียนไฟล์จริง เมื่อ CLI เวอร์ชันใหม่กว่ามาเจอไฟล์ `lib`/hook ที่ถูกป้องกันไว้และ `registryVersion` เก่ากว่าเวอร์ชันที่กำลังรัน จะเตือนว่าไฟล์อาจล้าสมัยเทียบกับ template รุ่นปัจจุบัน ดูรายละเอียดฟิลด์นี้ได้ที่ [ข้อมูลอ้างอิงการตั้งค่า](https://react-thai-zip.vercel.app/docs/reference/config)

**วิธีแก้:** ตรวจ diff ของไฟล์นั้นกับ template เวอร์ชันล่าสุดก่อนถ้าต้องการ แล้วรันคำสั่งที่ข้อความแนะนำไว้ตรง ๆ เพื่อ refresh:

```npm
npx react-thaizip add utils --overwrite
```

หลังจากไฟล์ถูกเขียนใหม่สำเร็จ `registryVersion` จะถูกอัปเดตให้ตรงกับเวอร์ชัน CLI ปัจจุบันโดยอัตโนมัติ คำเตือนนี้จะไม่ปรากฏอีกจนกว่าจะอัปเกรด CLI รอบถัดไป

## Import paths ไม่ตรงที่คาด

**ปัญหา:** คอมโพเนนต์ที่ scaffold import `utils` หรือ hook จาก relative path ที่ผิดสำหรับ layout ปัจจุบัน

**สาเหตุ:** `add` คำนวณ imports จาก working directory และ `componentDir`, `libDir`, `hooksDir` ใน config อาจเรียก CLI จาก directory ผิดหรือ path ใน config ไม่ตรงกับโปรเจกต์

**วิธีแก้:** เปิด `thaizip.config.json` และตรวจสอบ directory ทั้งสามให้เป็น path เทียบจากรากโปรเจกต์ จากนั้น `cd` ไปที่รากนั้นและ scaffold ใหม่ ถ้ามีคอมโพเนนต์อยู่แล้ว ต้องตั้งใจใช้ `--overwrite` เพื่อให้ไฟล์และ imports ถูกเขียนใหม่:

```npm
npx react-thaizip add autocomplete --overwrite
```

เนื่องจาก `utils` และ hook ถูกดึงมาเป็น registry dependency ของคอมโพเนนต์โดยอัตโนมัติ คำสั่งข้างต้นจะเขียนทับไฟล์ `lib`/hook ที่มีอยู่ด้วยเช่นกัน ไม่ใช่แค่ไฟล์คอมโพเนนต์ ไฟล์เหล่านี้ไม่มีเนื้อหาที่อ้างอิง path จาก config จึงไม่เกี่ยวกับปัญหา import path แต่จะถูกรีเฟรชไปพร้อมกัน

## ไม่มี `thaizip.config.json`

**ปัญหา:** `add` ถาม `No thaizip.config.json found. Run init now?`

**สาเหตุ:** ยังไม่ได้รัน `init`, ไฟล์ถูกลบ หรือกำลังรันจาก directory อื่น

**วิธีแก้:** ยอมรับ prompt เพื่อให้ `add` เรียก `init` แล้วทำงานต่อ หรือรันคำสั่งนี้เองจากรากโปรเจกต์ก่อนเรียก `add`:

```npm
npx react-thaizip init
npx react-thaizip add autocomplete
```

ดูคำสั่งและ flags ทั้งหมดได้ที่ [ข้อมูลอ้างอิง CLI](https://react-thai-zip.vercel.app/docs/reference/cli)

---

# Introduction

> react-thaizip adds Thai address input components to React and Next.js projects by copying their source into your project for you to own.

Source: https://react-thai-zip.vercel.app/en/docs

```tsx title="autocomplete-en.tsx"
'use client'

import { ThaiAddressAutocomplete } from '@/components/thai-address-autocomplete'

export default function AutocompleteEn() {
  return <ThaiAddressAutocomplete locale="en" onValueChange={(address) => console.log(address)} />
}
```

**react-thaizip is not a component library you install as a dependency.** It is a CLI that copies Thai address component source into your project, the same way [shadcn/ui](https://ui.shadcn.com/) does. Once added, the files are your code: change the markup, styles, or behavior directly.

The example above is `ThaiAddressAutocomplete` from the real template the CLI copies, not a copy written for these docs. Select "View Code" to see the code it uses.

## Principles

- **You own the code**: components live in your project, so customizing them needs no style overrides or wrappers.
- **Data included**: search subdistricts, districts, provinces, and postal codes in Thai or English with [thaizip](https://naay99999.github.io/thai-zip/en/), without calling an external API.
- **Fits your design system**: get the vanilla style (Base UI + Tailwind) or files composed from your shadcn/ui primitives (Base UI, Radix, or React Aria), whichever `init` detects.
- **AI-ready**: every page has a Markdown version with the example code, plus [`/llms.txt`](https://react-thai-zip.vercel.app/llms.txt) and [`/llms-full.txt`](https://react-thai-zip.vercel.app/llms-full.txt). See [Use react-thaizip with AI](https://react-thai-zip.vercel.app/en/docs/guides/using-with-ai).

## Components

| Component | Use it when |
| --- | --- |
| [Autocomplete](https://react-thai-zip.vercel.app/en/docs/components/autocomplete) | People type to search by subdistrict, district, province, or postal code |
| [Cascade select](https://react-thai-zip.vercel.app/en/docs/components/cascade-select) | People pick province → district → subdistrict in order |
| [Address form](https://react-thai-zip.vercel.app/en/docs/components/address-form) | You need house number, moo, soi, and street plus the address hierarchy in one form |
| [Address display](https://react-thai-zip.vercel.app/en/docs/components/address-display) | You show a saved address read-only |
| [Address form field](https://react-thai-zip.vercel.app/en/docs/components/address-form-field) | You bind the cascade select to react-hook-form |

## Quick start

```npm
npx react-thaizip init
npx react-thaizip add autocomplete
```

`init` checks Tailwind and shadcn/ui and writes `thaizip.config.json`; `add` copies the component with its helper files and installs dependencies. See [Get started](https://react-thai-zip.vercel.app/en/docs/getting-started) for each step.

Need the lower-level address search API to build your own UI? See the [thaizip Core API](https://naay99999.github.io/thai-zip/en/).

---

# Get started

> Scaffold your first Thai address component into a project you own

Source: https://react-thai-zip.vercel.app/en/docs/getting-started

react-thaizip is a scaffold CLI, not a runtime component package: it writes component source into your project, and you own every generated file.

## Prerequisites

Use Node.js 18 or later and an existing React or Next.js project with Tailwind CSS v3 or v4 already installed. `init` detects Tailwind; it does not install it for you. If detection fails, follow the [Tailwind troubleshooting steps](https://react-thai-zip.vercel.app/en/docs/troubleshooting#init-cannot-detect-tailwind).

## Create your configuration

```npm
npx react-thaizip init
```

This command checks the project structure and Tailwind setup, chooses the package manager, and creates `thaizip.config.json`. That file records the component, hook, and `lib` directories used by `add`. See every field and detection rule in the [configuration reference](https://react-thai-zip.vercel.app/en/docs/reference/config).

If your project has a supported shadcn/ui `components.json`, `init` also records whether its primitives use Base UI, Radix, or React Aria. Otherwise it selects the vanilla Base UI component style.

`init` also adds CSS custom-property design tokens to the global stylesheet. With Tailwind v3, it prints a `theme.extend` snippet that you add to your Tailwind configuration yourself.

## Add autocomplete

```npm
npx react-thaizip add autocomplete
```

The generated files look like this by default (the component location can differ with your project structure or the choice made during `init`):

```text
components/
  thai-address-autocomplete.tsx
hooks/
  use-thai-address-index.ts
lib/
  utils.ts
```

The command also installs any component dependencies that are not already present. See the [CLI reference](https://react-thai-zip.vercel.app/en/docs/reference/cli) for more targets, aliases, and flags.

Follow the import path printed by `add`; it reflects the directory you chose during `init`. Examples in these docs use the shadcn/ui-style `@/components/...` alias; if your project has no such alias or keeps the files elsewhere, swap in the path `add` printed.

## Use your first component

```tsx
import { ThaiAddressAutocomplete } from '@/components/thai-address-autocomplete'

export function AddressField() {
  return <ThaiAddressAutocomplete onValueChange={(address) => console.log(address)} />
}
```

Next, see the [cascade select](https://react-thai-zip.vercel.app/en/docs/components/cascade-select) guide for ordered address entry, then [Forms](https://react-thai-zip.vercel.app/en/docs/guides/forms) to submit address values with a browser form.

---

# All components

> Every Thai address component the CLI can add to your project

Source: https://react-thai-zip.vercel.app/en/docs/components

Add any component with `npx react-thaizip add <name>`. Every component except the read-only address display ships in the vanilla style plus one style per shadcn/ui base (Base UI, Radix UI, React Aria); pick the style at the top of a component page to preview it.

<Cards>
  <Card title="Autocomplete" href="https://react-thai-zip.vercel.app/en/docs/components/autocomplete">
    Type to search by sub-district, district, province, or postal code.
  </Card>
  <Card title="Cascade select" href="https://react-thai-zip.vercel.app/en/docs/components/cascade-select">
    Pick province → district → sub-district in order.
  </Card>
  <Card title="Address form" href="https://react-thai-zip.vercel.app/en/docs/components/address-form">
    House number, moo, soi, and street plus the address hierarchy in one form.
  </Card>
  <Card title="Address display" href="https://react-thai-zip.vercel.app/en/docs/components/address-display">
    Show a saved address read-only, with no address-index loading.
  </Card>
  <Card title="Address form field" href="https://react-thai-zip.vercel.app/en/docs/components/address-form-field">
    The cascade select bound to react-hook-form's validation and state.
  </Card>
</Cards>

---

# Address autocomplete

> Search Thai addresses from typed text and receive a structured address value

Source: https://react-thai-zip.vercel.app/en/docs/components/autocomplete

`ThaiAddressAutocomplete` is for forms where people can search an address by sub-district, district, province, or postal code.

```tsx title="autocomplete-en.tsx"
'use client'

import { ThaiAddressAutocomplete } from '@/components/thai-address-autocomplete'

export default function AutocompleteEn() {
  return <ThaiAddressAutocomplete locale="en" onValueChange={(address) => console.log(address)} />
}
```

## Installation

**CLI**

```bash
npx react-thaizip add autocomplete
```

The CLI picks files based on the style `init` recorded: projects with a shadcn/ui `components.json` get files composed from shadcn primitives (Base UI, Radix, or React Aria); everything else gets the vanilla files.

**Manual — Vanilla**

1. Install the dependencies: `npm install clsx tailwind-merge thaizip @base-ui/react`
1. Copy these files into your project:
   - `lib/utils.ts` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/lib/utils.ts)
   - `hooks/use-thai-address-index.ts` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-index.ts)
   - `components/thai-address-autocomplete.tsx` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/thai-address-autocomplete.tsx)
1. Update the `@/lib/...` and `@/hooks/...` imports to match where the files live in your project (the CLI does this for you).
1. The vanilla style also needs the design tokens in your global stylesheet — see [Customization](https://react-thai-zip.vercel.app/en/docs/guides/customization#tokens-inserted-by-init).

**Manual — shadcn/ui · Base UI**

1. Install the dependencies: `npm install clsx tailwind-merge thaizip`
1. Add the shadcn/ui primitives the component uses: `npx shadcn@latest add popover command button`
1. Copy these files into your project:
   - `lib/utils.ts` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/lib/utils.ts)
   - `hooks/use-thai-address-index.ts` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-index.ts)
   - `components/thai-address-autocomplete.tsx` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/shadcn/base/thai-address-autocomplete.tsx)
1. Update the `@/lib/...` and `@/hooks/...` imports to match where the files live in your project (the CLI does this for you).

**Manual — shadcn/ui · Radix**

1. Install the dependencies: `npm install clsx tailwind-merge thaizip`
1. Add the shadcn/ui primitives the component uses: `npx shadcn@latest add popover command button`
1. Copy these files into your project:
   - `lib/utils.ts` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/lib/utils.ts)
   - `hooks/use-thai-address-index.ts` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-index.ts)
   - `components/thai-address-autocomplete.tsx` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/shadcn/radix/thai-address-autocomplete.tsx)
1. Update the `@/lib/...` and `@/hooks/...` imports to match where the files live in your project (the CLI does this for you).

**Manual — shadcn/ui · React Aria**

1. Install the dependencies: `npm install clsx tailwind-merge thaizip`
1. Add the shadcn/ui primitives the component uses: `npx shadcn@latest add popover command button`
1. Copy these files into your project:
   - `lib/utils.ts` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/lib/utils.ts)
   - `hooks/use-thai-address-index.ts` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-index.ts)
   - `components/thai-address-autocomplete.tsx` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/shadcn/aria/thai-address-autocomplete.tsx)
1. Update the `@/lib/...` and `@/hooks/...` imports to match where the files live in your project (the CLI does this for you).

## Usage

```tsx
import { ThaiAddressAutocomplete } from '@/components/thai-address-autocomplete'
```

```tsx
<ThaiAddressAutocomplete onValueChange={(address) => console.log(address)} />
```

`onValueChange` receives `ResolvedThaiAddress | null`. Selecting a suggestion yields a structured address; typing an empty query clears an existing resolved selection with `null`.

## Examples

### Thai search

Without `locale`, labels, the placeholder, and suggestions are in Thai (the default `locale="th"`). The returned data includes both Thai and English names either way.

```tsx title="autocomplete-demo.tsx"
'use client'

import { ThaiAddressAutocomplete } from '@/components/thai-address-autocomplete'

export default function AutocompleteDemo() {
  return <ThaiAddressAutocomplete onValueChange={(address) => console.log(address)} />
}
```

### Label

The component renders no label of its own. Wrap it in a `<label>` to give the field an accessible name, and add a description underneath.

```tsx title="autocomplete-label-en.tsx"
import { ThaiAddressAutocomplete } from '@/components/thai-address-autocomplete'

export default function AutocompleteLabelEn() {
  return (
    <div className="flex flex-col gap-2">
      <label className="flex flex-col gap-2 text-sm font-medium">
        Shipping address
        <ThaiAddressAutocomplete locale="en" />
      </label>
      <p className="text-sm text-muted-foreground">Type a sub-district, district, or postal code, then pick a result.</p>
    </div>
  )
}
```

### Controlled

Pass `value` to control the selection, or `defaultValue` to seed uncontrolled state. This example includes a clear button for the controlled value and shows the callback output.

```tsx title="autocomplete-controlled-en.tsx"
'use client'

import { useState } from 'react'
import type { ResolvedThaiAddress } from 'thaizip'
import { ThaiAddressAutocomplete } from '@/components/thai-address-autocomplete'

export default function AutocompleteControlledEn() {
  const [address, setAddress] = useState<ResolvedThaiAddress | null>(null)

  return (
    <div className="flex flex-col gap-3">
      <ThaiAddressAutocomplete locale="en" value={address} onValueChange={setAddress} />
      <div>
        <button type="button" className="rounded-md border border-input bg-background px-3 py-1.5 text-sm hover:bg-accent" onClick={() => setAddress(null)}>
          Clear
        </button>
      </div>
      <p className="text-sm font-medium">Current value</p>
      <pre className="max-h-72 overflow-auto rounded-md border bg-muted p-3 text-xs">{JSON.stringify(address, null, 2)}</pre>
    </div>
  )
}
```

### Disabled

Pass `disabled` to turn off the search field.

```tsx title="autocomplete-disabled.tsx"
'use client'

import { ThaiAddressAutocomplete } from '@/components/thai-address-autocomplete'

export default function AutocompleteDisabled() {
  return <ThaiAddressAutocomplete disabled />
}
```

### Invalid

Pass `aria-invalid` to mark the field invalid, for example after failed form validation.

```tsx title="autocomplete-invalid.tsx"
'use client'

import { ThaiAddressAutocomplete } from '@/components/thai-address-autocomplete'

export default function AutocompleteInvalid() {
  return <ThaiAddressAutocomplete aria-invalid />
}
```

### Custom text

Use `texts` to override the placeholder and the loading/error/empty messages. Keys you don't pass keep the `locale` defaults.

```tsx title="autocomplete-texts-en.tsx"
import { ThaiAddressAutocomplete } from '@/components/thai-address-autocomplete'

export default function AutocompleteTextsEn() {
  return (
    <ThaiAddressAutocomplete
      locale="en"
      texts={{
        placeholder: 'Search by postal code, e.g. 10110',
        emptyText: 'No match. Try a district name instead.',
      }}
    />
  )
}
```

### Custom styling

Each part of the component has its own class prop (see [API Reference](#api-reference)). Classes are merged with `cn`, so they override the defaults.

```tsx title="autocomplete-styled-en.tsx"
import { ThaiAddressAutocomplete } from '@/components/thai-address-autocomplete'

export default function AutocompleteStyledEn() {
  return (
    <ThaiAddressAutocomplete
      locale="en"
      inputClassName="rounded-full"
      popupClassName="rounded-2xl"
      itemClassName="rounded-xl"
    />
  )
}
```

### Form

Pass `name` to submit the selected address with a regular form: the component renders four hidden inputs (`address-subdistrict`, `address-district`, `address-province`, `address-zipcode`). `required` blocks submission until an address is selected. See [Forms](https://react-thai-zip.vercel.app/en/docs/guides/forms) for more patterns.

```tsx title="form-autocomplete-en.tsx"
'use client'

import { useRef, useState, type FormEvent } from 'react'
import { ThaiAddressAutocomplete } from '@/components/thai-address-autocomplete'

export default function FormAutocompleteEn() {
  const ref = useRef<HTMLInputElement>(null)
  const [payload, setPayload] = useState<Record<string, FormDataEntryValue> | null>(null)

  function handleSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault()
    setPayload(Object.fromEntries(new FormData(event.currentTarget)))
  }

  return (
    <form className="flex flex-col gap-3" onSubmit={handleSubmit}>
      <ThaiAddressAutocomplete
        ref={ref}
        name="address"
        required
        locale="en"
      />
      <div className="flex gap-2">
        <button type="button" className="rounded-md border border-input bg-background px-3 py-1.5 text-sm hover:bg-accent" onClick={() => ref.current?.focus()}>
          Focus address
        </button>
        <button type="submit" className="rounded-md border border-input bg-background px-3 py-1.5 text-sm hover:bg-accent">
          Submit
        </button>
      </div>
      <p className="text-sm font-medium">Submitted FormData</p>
      <pre className="max-h-72 overflow-auto rounded-md border bg-muted p-3 text-xs">{JSON.stringify(payload, null, 2)}</pre>
    </form>
  )
}
```

## API Reference

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `ResolvedThaiAddress \| null` | — | Controlled value; pass `null` to clear it. |
| `defaultValue` | `ResolvedThaiAddress \| null` | — | Uncontrolled seed value; also pre-fills the initial search text. |
| `onValueChange` | `(address: ResolvedThaiAddress \| null) => void` | — | Called when an address is selected or cleared. |
| `name` | `string` | — | Renders four hidden inputs: `<name>-subdistrict`, `-district`, `-province`, `-zipcode`. |
| `locale` | `'th' \| 'en'` | `'th'` | Locale of suggestions and default text. |
| `texts` | `Partial<ThaiAddressAutocompleteTexts>` | — | Overrides `placeholder`, `clearAriaLabel`, `loadingText`, `errorText`, `retryLabel`, `emptyText`. |
| `indexLoad` | `'mount' \| 'visible'` | `'mount'` | Load the address data on mount, or only once the field scrolls into view. |
| `limit` | `number` | `10` | Maximum number of suggestions. |
| `debounce` | `number` | `200` | Delay before searching, in milliseconds. |
| `threshold` | `number` | `0.4` | Minimum match score (0–1) for a suggestion to be shown. |
| `disabled` | `boolean` | `false` | Disables the field. |
| `required` | `boolean` | — | Marks the field as required. |
| `aria-invalid` | `React.AriaAttributes['aria-invalid']` | — | Marks the field invalid. |
| `onBlur` | `React.FocusEventHandler` | — | Vanilla: fires when the input loses focus. shadcn/ui: fires on the trigger button (`HTMLButtonElement`). |
| `onError` | `(error: Error) => void` | — | Runs if the address index cannot load. |
| `className` | `string` | — | Root wrapper. |
| `inputClassName` | `string` | — | Search input. |
| `popupClassName` | `string` | — | Results popup. |
| `itemClassName` | `string` | — | Each result item. |
| `ref` | `React.Ref<HTMLInputElement>` | — | Forwarded to the input. In the shadcn/ui styles the input lives inside the popover, so `ref.current` is `null` while it is closed. |

`limit`, `debounce`, and `threshold` are passed straight to thaizip's `useThaiAddressAutocomplete` hook.

## Related

See [Forms](https://react-thai-zip.vercel.app/en/docs/guides/forms) to submit address values and [Customization](https://react-thai-zip.vercel.app/en/docs/guides/customization) to change the presentation. For ordered address selection, see [cascade select](https://react-thai-zip.vercel.app/en/docs/components/cascade-select).

---

# Address cascade select

> Choose province, district, and sub-district in order

Source: https://react-thai-zip.vercel.app/en/docs/components/cascade-select

`ThaiAddressCascadeSelect` is for forms where people choose an address in the order province → district → sub-district.

```tsx title="cascade-select-en.tsx"
'use client'

import { ThaiAddressCascadeSelect } from '@/components/thai-address-cascade-select'

export default function CascadeSelectEn() {
  return <ThaiAddressCascadeSelect locale="en" onValueChange={(address) => console.log(address)} />
}
```

## Installation

**CLI**

```bash
npx react-thaizip add cascade-select
```

The CLI picks files based on the style `init` recorded: projects with a shadcn/ui `components.json` get files composed from shadcn primitives (Base UI, Radix, or React Aria); everything else gets the vanilla files.

**Manual — Vanilla**

1. Install the dependencies: `npm install clsx tailwind-merge thaizip @base-ui/react`
1. Copy these files into your project:
   - `lib/utils.ts` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/lib/utils.ts)
   - `hooks/use-thai-address-index.ts` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-index.ts)
   - `hooks/use-thai-address-cascade.ts` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-cascade.ts)
   - `components/thai-address-cascade-select.tsx` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/thai-address-cascade-select.tsx)
1. Update the `@/lib/...` and `@/hooks/...` imports to match where the files live in your project (the CLI does this for you).
1. The vanilla style also needs the design tokens in your global stylesheet — see [Customization](https://react-thai-zip.vercel.app/en/docs/guides/customization#tokens-inserted-by-init).

**Manual — shadcn/ui · Base UI**

1. Install the dependencies: `npm install clsx tailwind-merge thaizip`
1. Add the shadcn/ui primitives the component uses: `npx shadcn@latest add select label button input`
1. Copy these files into your project:
   - `lib/utils.ts` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/lib/utils.ts)
   - `hooks/use-thai-address-index.ts` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-index.ts)
   - `hooks/use-thai-address-cascade.ts` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-cascade.ts)
   - `components/thai-address-cascade-select.tsx` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/shadcn/base/thai-address-cascade-select.tsx)
1. Update the `@/lib/...` and `@/hooks/...` imports to match where the files live in your project (the CLI does this for you).

**Manual — shadcn/ui · Radix**

1. Install the dependencies: `npm install clsx tailwind-merge thaizip`
1. Add the shadcn/ui primitives the component uses: `npx shadcn@latest add select label button input`
1. Copy these files into your project:
   - `lib/utils.ts` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/lib/utils.ts)
   - `hooks/use-thai-address-index.ts` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-index.ts)
   - `hooks/use-thai-address-cascade.ts` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-cascade.ts)
   - `components/thai-address-cascade-select.tsx` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/shadcn/radix/thai-address-cascade-select.tsx)
1. Update the `@/lib/...` and `@/hooks/...` imports to match where the files live in your project (the CLI does this for you).

**Manual — shadcn/ui · React Aria**

1. Install the dependencies: `npm install clsx tailwind-merge thaizip`
1. Add the shadcn/ui primitives the component uses: `npx shadcn@latest add select label button input`
1. Copy these files into your project:
   - `lib/utils.ts` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/lib/utils.ts)
   - `hooks/use-thai-address-index.ts` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-index.ts)
   - `hooks/use-thai-address-cascade.ts` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-cascade.ts)
   - `components/thai-address-cascade-select.tsx` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/shadcn/aria/thai-address-cascade-select.tsx)
1. Update the `@/lib/...` and `@/hooks/...` imports to match where the files live in your project (the CLI does this for you).

## Usage

```tsx
import { ThaiAddressCascadeSelect } from '@/components/thai-address-cascade-select'
```

```tsx
<ThaiAddressCascadeSelect onValueChange={(address) => console.log(address)} />
```

`onValueChange` receives `ResolvedThaiAddress | null` once all three levels have been chosen.

### Changing a parent choice

After a complete selection, changing the province or district emits `onValueChange(null)` and clears downstream selections that no longer match. The person then chooses the remaining address levels again.

## Examples

### Thai choices

Without `locale`, labels and options are in Thai (the default `locale="th"`).

```tsx title="cascade-select-demo.tsx"
'use client'

import { ThaiAddressCascadeSelect } from '@/components/thai-address-cascade-select'

export default function CascadeSelectDemo() {
  return <ThaiAddressCascadeSelect onValueChange={(address) => console.log(address)} />
}
```

### Controlled

Pass `value` to control the full selection, or `defaultValue` to seed the selected address. This example includes a clear button and shows the callback value.

```tsx title="cascade-select-controlled-en.tsx"
'use client'

import { useState } from 'react'
import type { ResolvedThaiAddress } from 'thaizip'
import { ThaiAddressCascadeSelect } from '@/components/thai-address-cascade-select'

export default function CascadeSelectControlledEn() {
  const [address, setAddress] = useState<ResolvedThaiAddress | null>(null)

  return (
    <div className="flex flex-col gap-3">
      <ThaiAddressCascadeSelect locale="en" value={address} onValueChange={setAddress} />
      <div>
        <button type="button" className="rounded-md border border-input bg-background px-3 py-1.5 text-sm hover:bg-accent" onClick={() => setAddress(null)}>
          Clear
        </button>
      </div>
      <p className="text-sm font-medium">Current value</p>
      <pre className="max-h-72 overflow-auto rounded-md border bg-muted p-3 text-xs">{JSON.stringify(address, null, 2)}</pre>
    </div>
  )
}
```

### Disabled

Pass `disabled` to disable every select.

```tsx title="cascade-select-disabled.tsx"
'use client'

import { ThaiAddressCascadeSelect } from '@/components/thai-address-cascade-select'

export default function CascadeSelectDisabled() {
  return <ThaiAddressCascadeSelect disabled />
}
```

### Invalid

Pass `aria-invalid` to mark all three triggers invalid.

```tsx title="cascade-select-invalid.tsx"
'use client'

import { ThaiAddressCascadeSelect } from '@/components/thai-address-cascade-select'

export default function CascadeSelectInvalid() {
  return <ThaiAddressCascadeSelect aria-invalid />
}
```

### Custom text

Use `texts` to override labels and placeholders, for example to match Bangkok's khet/khwaeng wording. Keys you don't pass keep the `locale` defaults.

```tsx title="cascade-select-texts-en.tsx"
import { ThaiAddressCascadeSelect } from '@/components/thai-address-cascade-select'

export default function CascadeSelectTextsEn() {
  return (
    <ThaiAddressCascadeSelect
      locale="en"
      texts={{
        provinceLabel: 'Delivery province',
        provincePlaceholder: 'Choose a province',
        districtLabel: 'District / khet',
        subdistrictLabel: 'Sub-district / khwaeng',
      }}
    />
  )
}
```

### Form

Pass `name` to submit the selection with a regular form: the component renders four hidden inputs (`address-subdistrict`, `address-district`, `address-province`, `address-zipcode`). `required` blocks submission until all three levels are chosen. See [Forms](https://react-thai-zip.vercel.app/en/docs/guides/forms) for more patterns.

```tsx title="form-cascade-select-en.tsx"
'use client'

import { useRef, useState, type FormEvent } from 'react'
import { ThaiAddressCascadeSelect } from '@/components/thai-address-cascade-select'

export default function FormCascadeSelectEn() {
  const ref = useRef<HTMLButtonElement>(null)
  const [payload, setPayload] = useState<Record<string, FormDataEntryValue> | null>(null)

  function handleSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault()
    setPayload(Object.fromEntries(new FormData(event.currentTarget)))
  }

  return (
    <form className="flex flex-col gap-3" onSubmit={handleSubmit}>
      <ThaiAddressCascadeSelect
        ref={ref}
        name="address"
        required
        locale="en"
      />
      <div className="flex gap-2">
        <button type="button" className="rounded-md border border-input bg-background px-3 py-1.5 text-sm hover:bg-accent" onClick={() => ref.current?.focus()}>
          Focus address
        </button>
        <button type="submit" className="rounded-md border border-input bg-background px-3 py-1.5 text-sm hover:bg-accent">
          Submit
        </button>
      </div>
      <p className="text-sm font-medium">Submitted FormData</p>
      <pre className="max-h-72 overflow-auto rounded-md border bg-muted p-3 text-xs">{JSON.stringify(payload, null, 2)}</pre>
    </form>
  )
}
```

## API Reference

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `ResolvedThaiAddress \| null` | — | Controlled value; pass `null` to clear it. |
| `defaultValue` | `ResolvedThaiAddress \| null` | — | Uncontrolled seed value; pre-selects the complete address chain. |
| `onValueChange` | `(address: ResolvedThaiAddress \| null) => void` | — | Called when an address is completed or cleared. |
| `name` | `string` | — | Renders four hidden inputs: `<name>-subdistrict`, `-district`, `-province`, `-zipcode`. |
| `locale` | `'th' \| 'en'` | `'th'` | Label and option locale. |
| `texts` | `Partial<ThaiAddressCascadeSelectTexts>` | — | Overrides the `*Label`/`*Placeholder` text of each level plus `loadingText`, `errorText`, `retryLabel`. |
| `indexLoad` | `'mount' \| 'visible'` | `'mount'` | Load the address data on mount, or only once the field scrolls into view. |
| `disabled` | `boolean` | `false` | Disables all selects. |
| `required` | `boolean` | — | Marks all three selects as required. |
| `aria-invalid` | `React.AriaAttributes['aria-invalid']` | — | Marks all three triggers invalid. |
| `onBlur` | `React.FocusEventHandler<HTMLButtonElement>` | — | Runs when the province trigger loses focus. |
| `onError` | `(error: Error) => void` | — | Runs if the address index cannot load. |
| `className` | `string` | — | Root grid wrapper. |
| `labelClassName` | `string` | — | Every field label. |
| `triggerClassName` | `string` | — | Select triggers and the postal-code input. |
| `popupClassName` | `string` | — | Option popups. |
| `itemClassName` | `string` | — | Each option item. |
| `ref` | `React.Ref<HTMLButtonElement>` | — | Forwarded to the province trigger, the cascade's primary control. |

Both `ref` and `onBlur` target the province trigger, not the district or sub-district trigger.

## Related

See [Forms](https://react-thai-zip.vercel.app/en/docs/guides/forms) to submit address values and [Customization](https://react-thai-zip.vercel.app/en/docs/guides/customization) to change the presentation. For free-text search, see [autocomplete](https://react-thai-zip.vercel.app/en/docs/components/autocomplete).

---

# Full address form

> Capture house number, moo, soi, and street alongside an ordered address selection in one form

Source: https://react-thai-zip.vercel.app/en/docs/components/address-form

`ThaiAddressForm` is for forms that need a full address: house number/moo/soi/street plus sub-district, district, and province. It embeds a [cascade select](https://react-thai-zip.vercel.app/en/docs/components/cascade-select) internally.

```tsx title="address-form-en.tsx"
'use client'

import { ThaiAddressForm } from '@/components/thai-address-form'

export default function AddressFormEn() {
  return <ThaiAddressForm locale="en" onValueChange={(address) => console.log(address)} />
}
```

## Installation

**CLI**

```bash
npx react-thaizip add address-form
```

The CLI picks files based on the style `init` recorded: projects with a shadcn/ui `components.json` get files composed from shadcn primitives (Base UI, Radix, or React Aria); everything else gets the vanilla files.

**Manual — Vanilla**

1. Install the dependencies: `npm install clsx tailwind-merge thaizip @base-ui/react`
1. Copy these files into your project:
   - `lib/utils.ts` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/lib/utils.ts)
   - `hooks/use-thai-address-index.ts` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-index.ts)
   - `hooks/use-thai-address-cascade.ts` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-cascade.ts)
   - `components/thai-address-cascade-select.tsx` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/thai-address-cascade-select.tsx)
   - `components/thai-address-form.tsx` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/thai-address-form.tsx)
1. Update the `@/lib/...` and `@/hooks/...` imports to match where the files live in your project (the CLI does this for you).
1. The vanilla style also needs the design tokens in your global stylesheet — see [Customization](https://react-thai-zip.vercel.app/en/docs/guides/customization#tokens-inserted-by-init).

**Manual — shadcn/ui · Base UI**

1. Install the dependencies: `npm install clsx tailwind-merge thaizip`
1. Add the shadcn/ui primitives the component uses: `npx shadcn@latest add select label button input`
1. Copy these files into your project:
   - `lib/utils.ts` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/lib/utils.ts)
   - `hooks/use-thai-address-index.ts` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-index.ts)
   - `hooks/use-thai-address-cascade.ts` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-cascade.ts)
   - `components/thai-address-cascade-select.tsx` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/shadcn/base/thai-address-cascade-select.tsx)
   - `components/thai-address-form.tsx` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/shadcn/base/thai-address-form.tsx)
1. Update the `@/lib/...` and `@/hooks/...` imports to match where the files live in your project (the CLI does this for you).

**Manual — shadcn/ui · Radix**

1. Install the dependencies: `npm install clsx tailwind-merge thaizip`
1. Add the shadcn/ui primitives the component uses: `npx shadcn@latest add select label button input`
1. Copy these files into your project:
   - `lib/utils.ts` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/lib/utils.ts)
   - `hooks/use-thai-address-index.ts` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-index.ts)
   - `hooks/use-thai-address-cascade.ts` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-cascade.ts)
   - `components/thai-address-cascade-select.tsx` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/shadcn/radix/thai-address-cascade-select.tsx)
   - `components/thai-address-form.tsx` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/shadcn/radix/thai-address-form.tsx)
1. Update the `@/lib/...` and `@/hooks/...` imports to match where the files live in your project (the CLI does this for you).

**Manual — shadcn/ui · React Aria**

1. Install the dependencies: `npm install clsx tailwind-merge thaizip`
1. Add the shadcn/ui primitives the component uses: `npx shadcn@latest add select label button input`
1. Copy these files into your project:
   - `lib/utils.ts` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/lib/utils.ts)
   - `hooks/use-thai-address-index.ts` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-index.ts)
   - `hooks/use-thai-address-cascade.ts` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-cascade.ts)
   - `components/thai-address-cascade-select.tsx` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/shadcn/aria/thai-address-cascade-select.tsx)
   - `components/thai-address-form.tsx` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/shadcn/aria/thai-address-form.tsx)
1. Update the `@/lib/...` and `@/hooks/...` imports to match where the files live in your project (the CLI does this for you).

## Usage

```tsx
import { ThaiAddressForm } from '@/components/thai-address-form'
```

```tsx
<ThaiAddressForm onValueChange={(address) => console.log(address)} />
```

`onValueChange` receives `FullThaiAddress | null`. It is only non-null once the house number is filled in and all three cascade levels are selected.

## Examples

### Thai input

Without `locale`, labels and options are in Thai (the default `locale="th"`), for both the free-text fields and the embedded cascade select.

```tsx title="address-form-demo.tsx"
'use client'

import { ThaiAddressForm } from '@/components/thai-address-form'

export default function AddressFormDemo() {
  return <ThaiAddressForm onValueChange={(address) => console.log(address)} />
}
```

### Controlled

Pass `value` to control the whole address, or `defaultValue` to seed it. An in-progress, not-yet-complete cascade selection is never cleared, even while the controlled `value` is `null`.

```tsx title="address-form-controlled-en.tsx"
'use client'

import { useState } from 'react'
import { ThaiAddressForm, type FullThaiAddress } from '@/components/thai-address-form'

export default function AddressFormControlledEn() {
  const [address, setAddress] = useState<FullThaiAddress | null>(null)

  return (
    <div className="flex flex-col gap-3">
      <ThaiAddressForm locale="en" value={address} onValueChange={setAddress} />
      <div>
        <button type="button" className="rounded-md border border-input bg-background px-3 py-1.5 text-sm hover:bg-accent" onClick={() => setAddress(null)}>
          Clear
        </button>
      </div>
      <p className="text-sm font-medium">Current value</p>
      <pre className="max-h-72 overflow-auto rounded-md border bg-muted p-3 text-xs">{JSON.stringify(address, null, 2)}</pre>
    </div>
  )
}
```

### Disabled

Pass `disabled` to disable every text field and select.

```tsx title="address-form-disabled.tsx"
'use client'

import { ThaiAddressForm } from '@/components/thai-address-form'

export default function AddressFormDisabled() {
  return <ThaiAddressForm disabled />
}
```

### Invalid

Pass `aria-invalid` to mark the four text inputs and the cascade's triggers invalid.

```tsx title="address-form-invalid.tsx"
'use client'

import { ThaiAddressForm } from '@/components/thai-address-form'

export default function AddressFormInvalid() {
  return <ThaiAddressForm aria-invalid />
}
```

### Custom text

Use `texts` for this component's own labels and placeholders, and `cascadeTexts` for the embedded cascade select's.

```tsx title="address-form-texts-en.tsx"
import { ThaiAddressForm } from '@/components/thai-address-form'

export default function AddressFormTextsEn() {
  return (
    <ThaiAddressForm
      locale="en"
      texts={{
        houseNoLabel: 'House / unit no.',
        houseNoPlaceholder: 'e.g. 99/9, unit 1203',
        streetLabel: 'Building / road',
      }}
    />
  )
}
```

### Form

A complete checkout-style form: recipient and phone fields next to the address, with Cancel and Submit buttons. Passing `name` renders eight hidden inputs, so a plain `FormData` holds the whole address. See [Forms](https://react-thai-zip.vercel.app/en/docs/guides/forms) for more patterns.

```tsx title="address-form-submit-en.tsx"
'use client'

import { useState, type FormEvent } from 'react'
import { ThaiAddressForm } from '@/components/thai-address-form'

export default function AddressFormSubmitEn() {
  const [payload, setPayload] = useState<Record<string, FormDataEntryValue> | null>(null)

  function handleSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault()
    setPayload(Object.fromEntries(new FormData(event.currentTarget)))
  }

  return (
    <form className="flex flex-col gap-4" onSubmit={handleSubmit} onReset={() => setPayload(null)}>
      <div className="grid gap-4 sm:grid-cols-2">
        <label className="flex flex-col gap-1.5 text-sm font-medium">
          Recipient name
          <input name="recipient" required className="h-9 rounded-md border border-input bg-background px-3 text-sm" />
        </label>
        <label className="flex flex-col gap-1.5 text-sm font-medium">
          Phone
          <input name="phone" type="tel" required className="h-9 rounded-md border border-input bg-background px-3 text-sm" />
        </label>
      </div>
      <ThaiAddressForm name="address" required locale="en" />
      <div className="flex justify-end gap-2">
        <button type="reset" className="rounded-md border border-input bg-background px-3 py-1.5 text-sm hover:bg-accent">
          Cancel
        </button>
        <button type="submit" className="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90">
          Save address
        </button>
      </div>
      <p className="text-sm font-medium">Submitted FormData</p>
      <pre className="max-h-72 overflow-auto rounded-md border bg-muted p-3 text-xs">{JSON.stringify(payload, null, 2)}</pre>
    </form>
  )
}
```

## API Reference

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `FullThaiAddress \| null` | — | Controlled value; pass `null` to clear it. |
| `defaultValue` | `FullThaiAddress \| null` | — | Uncontrolled seed value; pre-fills both the text fields and the embedded cascade. |
| `onValueChange` | `(address: FullThaiAddress \| null) => void` | — | Called once the house number is non-blank and all three levels are selected, or when the value becomes incomplete again. |
| `name` | `string` | — | Renders 8 hidden inputs: the cascade's own 4 (`-subdistrict`/`-district`/`-province`/`-zipcode`) plus this component's own 4 (`-houseno`/`-moo`/`-soi`/`-street`). |
| `locale` | `'th' \| 'en'` | `'th'` | Text and label locale for both this component and the embedded cascade. |
| `texts` | `Partial<ThaiAddressFormTexts>` | — | Overrides the house-number/moo/soi/street labels and placeholders. |
| `cascadeTexts` | `Partial<ThaiAddressCascadeSelectTexts>` | — | Forwarded as the embedded cascade select's own `texts` prop. |
| `indexLoad` | `'mount' \| 'visible'` | `'mount'` | Forwarded to the embedded cascade's address-index loader. |
| `disabled` | `boolean` | `false` | Disables every text field and the embedded cascade. |
| `required` | `boolean` | `false` | Marks the house-number field and the embedded cascade as required (moo/soi/street stay optional). |
| `aria-invalid` | `React.AriaAttributes['aria-invalid']` | — | Marks the four text inputs and the cascade's triggers invalid. |
| `onBlur` | `React.FocusEventHandler<HTMLInputElement>` | — | Runs only when the house-number input loses focus. |
| `onError` | `(error: Error) => void` | — | Runs if the address index cannot load (handled by the embedded cascade). |
| `className` | `string` | — | Root wrapper. |
| `labelClassName` | `string` | — | Labels of the text fields and the cascade. |
| `inputClassName` | `string` | — | The house-number/moo/soi/street inputs. |
| `triggerClassName` | `string` | — | The cascade's select triggers and postal-code input. |
| `popupClassName` | `string` | — | The cascade's option popups. |
| `itemClassName` | `string` | — | Each of the cascade's option items. |
| `ref` | `React.Ref<HTMLInputElement>` | — | Forwarded to the house-number input. |

## Related

See [Forms](https://react-thai-zip.vercel.app/en/docs/guides/forms) to submit address values and [Customization](https://react-thai-zip.vercel.app/en/docs/guides/customization) to change the presentation. For the embedded component on its own, see [cascade select](https://react-thai-zip.vercel.app/en/docs/components/cascade-select).

---

# Read-only address display

> Format an already-resolved Thai address as readable text, with no address-index loading

Source: https://react-thai-zip.vercel.app/en/docs/components/address-display

`ThaiAddressDisplay` is for pages that show an address you already have, such as an order summary or a user profile. It is purely presentational — no Base UI, and no address-index loading — so it ships in a single style.

```tsx title="address-display-en.tsx"
import { ThaiAddressDisplay, type ThaiAddressDisplayValue } from '@/components/thai-address-display'

const address: ThaiAddressDisplayValue = {
  tambon: 'คลองตันเหนือ',
  tambonEn: 'Khlong Tan Nuea',
  amphure: 'วัฒนา',
  amphureEn: 'Watthana',
  province: 'กรุงเทพมหานคร',
  provinceEn: 'Bangkok',
  zipCode: '10110',
  subdistrict: 'คลองตันเหนือ',
  subdistrictEn: 'Khlong Tan Nuea',
  district: 'วัฒนา',
  districtEn: 'Watthana',
  postalCode: '10110',
  houseNo: '99/9',
  moo: '4',
  soi: '2',
  street: 'สุขุมวิท',
}

export default function AddressDisplayEn() {
  return <ThaiAddressDisplay value={address} locale="en" />
}
```

## Installation

**CLI**

```bash
npx react-thaizip add address-display
```

The CLI picks files based on the style `init` recorded: projects with a shadcn/ui `components.json` get files composed from shadcn primitives (Base UI, Radix, or React Aria); everything else gets the vanilla files.

**Manual — Vanilla**

1. Install the dependencies: `npm install clsx tailwind-merge thaizip`
1. Copy these files into your project:
   - `lib/utils.ts` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/lib/utils.ts)
   - `components/thai-address-display.tsx` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/thai-address-display.tsx)
1. Update the `@/lib/...` and `@/hooks/...` imports to match where the files live in your project (the CLI does this for you).
1. The vanilla style also needs the design tokens in your global stylesheet — see [Customization](https://react-thai-zip.vercel.app/en/docs/guides/customization#tokens-inserted-by-init).

## Usage

```tsx
import { ThaiAddressDisplay } from '@/components/thai-address-display'
```

```tsx
<ThaiAddressDisplay value={address} />
```

`value` takes an already-resolved address (`ResolvedThaiAddress` plus optional `houseNo`/`moo`/`soi`/`street`, typed as `ThaiAddressDisplayValue`), or `null` when there is no address yet, which renders `emptyText` instead.

## Examples

### Multi-line

Use `mode="multi-line"` to render the house-number/moo/soi/street portion and the sub-district/district/province portion as separate rows.

```tsx title="address-display-multi-line-en.tsx"
import { ThaiAddressDisplay, type ThaiAddressDisplayValue } from '@/components/thai-address-display'

const address: ThaiAddressDisplayValue = {
  tambon: 'คลองตันเหนือ',
  tambonEn: 'Khlong Tan Nuea',
  amphure: 'วัฒนา',
  amphureEn: 'Watthana',
  province: 'กรุงเทพมหานคร',
  provinceEn: 'Bangkok',
  zipCode: '10110',
  subdistrict: 'คลองตันเหนือ',
  subdistrictEn: 'Khlong Tan Nuea',
  district: 'วัฒนา',
  districtEn: 'Watthana',
  postalCode: '10110',
  houseNo: '99/9',
  moo: '4',
  soi: '2',
  street: 'สุขุมวิท',
}

export default function AddressDisplayMultiLineEn() {
  return <ThaiAddressDisplay value={address} locale="en" mode="multi-line" />
}
```

### Thai output

Use `locale="th"` for Thai names. "หมู่"/"ซอย" automatically become the standard transliterations "Moo"/"Soi" only under `locale="en"` — the house number and street name always render exactly as supplied.

```tsx title="address-display-demo.tsx"
import { ThaiAddressDisplay, type ThaiAddressDisplayValue } from '@/components/thai-address-display'

const address: ThaiAddressDisplayValue = {
  tambon: 'คลองตันเหนือ',
  tambonEn: 'Khlong Tan Nuea',
  amphure: 'วัฒนา',
  amphureEn: 'Watthana',
  province: 'กรุงเทพมหานคร',
  provinceEn: 'Bangkok',
  zipCode: '10110',
  subdistrict: 'คลองตันเหนือ',
  subdistrictEn: 'Khlong Tan Nuea',
  district: 'วัฒนา',
  districtEn: 'Watthana',
  postalCode: '10110',
  houseNo: '99/9',
  moo: '4',
  soi: '2',
  street: 'สุขุมวิท',
}

export default function AddressDisplayDemo() {
  return <ThaiAddressDisplay value={address} />
}
```

### No house-level fields

When `value` has none of `houseNo`/`moo`/`soi`/`street`, only the sub-district/district/province portion is rendered.

```tsx title="address-display-minimal-en.tsx"
import { ThaiAddressDisplay, type ThaiAddressDisplayValue } from '@/components/thai-address-display'

const address: ThaiAddressDisplayValue = {
  tambon: 'คลองตันเหนือ',
  tambonEn: 'Khlong Tan Nuea',
  amphure: 'วัฒนา',
  amphureEn: 'Watthana',
  province: 'กรุงเทพมหานคร',
  provinceEn: 'Bangkok',
  zipCode: '10110',
  subdistrict: 'คลองตันเหนือ',
  subdistrictEn: 'Khlong Tan Nuea',
  district: 'วัฒนา',
  districtEn: 'Watthana',
  postalCode: '10110',
}

export default function AddressDisplayMinimalEn() {
  return <ThaiAddressDisplay value={address} locale="en" />
}
```

### Empty

Pass `value={null}` to render `emptyText` instead (the default is "No address" or "ไม่มีที่อยู่", based on `locale`).

```tsx title="address-display-empty-en.tsx"
import { ThaiAddressDisplay } from '@/components/thai-address-display'

export default function AddressDisplayEmptyEn() {
  return <ThaiAddressDisplay value={null} locale="en" />
}
```

### Custom styling

Use `className` for the `<address>` element and `lineClassName` for each row in multi-line mode — here, a card with an emphasized first line.

```tsx title="address-display-styled-en.tsx"
import { ThaiAddressDisplay, type ThaiAddressDisplayValue } from '@/components/thai-address-display'

const address: ThaiAddressDisplayValue = {
  tambon: 'คลองตันเหนือ',
  tambonEn: 'Khlong Tan Nuea',
  amphure: 'วัฒนา',
  amphureEn: 'Watthana',
  province: 'กรุงเทพมหานคร',
  provinceEn: 'Bangkok',
  zipCode: '10110',
  subdistrict: 'คลองตันเหนือ',
  subdistrictEn: 'Khlong Tan Nuea',
  district: 'วัฒนา',
  districtEn: 'Watthana',
  postalCode: '10110',
  houseNo: '99/9',
  moo: '4',
  soi: '2',
  street: 'สุขุมวิท',
}

export default function AddressDisplayStyledEn() {
  return (
    <ThaiAddressDisplay
      value={address}
      locale="en"
      mode="multi-line"
      className="rounded-lg border p-4 not-italic"
      lineClassName="first:font-medium"
    />
  )
}
```

## API Reference

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `ThaiAddressDisplayValue \| null` | — | **Required.** The address to render; `null` renders `emptyText` instead. |
| `locale` | `'th' \| 'en'` | `'th'` | Drives the locality label and the default `emptyText`. |
| `mode` | `'single-line' \| 'multi-line'` | `'single-line'` | `'single-line'` joins the street portion and the locality with `', '`; `'multi-line'` renders them as two rows. |
| `emptyText` | `string` | `'ไม่มีที่อยู่'` / `'No address'` | Replacement text when `value` is `null`, based on `locale`. |
| `className` | `string` | — | The root `<address>`. |
| `lineClassName` | `string` | — | Each row in `'multi-line'` mode. |
| `ref` | `React.Ref<HTMLElement>` | — | Forwarded to the `<address>` element. |

## Related

See [Customization](https://react-thai-zip.vercel.app/en/docs/guides/customization) to change the presentation. To let people choose or edit an address instead of only displaying it, see [address form](https://react-thai-zip.vercel.app/en/docs/components/address-form) or [cascade select](https://react-thai-zip.vercel.app/en/docs/components/cascade-select).

---

# react-hook-form address field

> Wrap the cascade select in react-hook-form's Controller to bind it to an existing form's validation and state

Source: https://react-thai-zip.vercel.app/en/docs/components/address-form-field

`ThaiAddressFormField` is for forms that already use [react-hook-form](https://react-hook-form.com/) and want the address bound directly into form state, instead of reading it from hidden inputs. Its prop shape is deliberately different from the other components: pass `control`/`name`/`rules` the same way you would to react-hook-form's own `useController`, and there is no `value`/`onValueChange`/hidden inputs.

```tsx title="address-form-field-en.tsx"
'use client'

import { useState } from 'react'
import { useForm } from 'react-hook-form'
import type { ResolvedThaiAddress } from 'thaizip'
import { ThaiAddressFormField } from '@/components/thai-address-form-field'

type FormValues = {
  address: ResolvedThaiAddress | null
}

export default function AddressFormFieldEn() {
  const { control, handleSubmit } = useForm<FormValues>({ defaultValues: { address: null } })
  const [submitted, setSubmitted] = useState<ResolvedThaiAddress | null>(null)

  return (
    <form className="flex flex-col gap-3" onSubmit={handleSubmit((data) => setSubmitted(data.address))}>
      <ThaiAddressFormField
        control={control}
        name="address"
        locale="en"
      />
      <div>
        <button type="submit" className="rounded-md border border-input bg-background px-3 py-1.5 text-sm hover:bg-accent">
          Submit
        </button>
      </div>
      <p className="text-sm font-medium">Submitted value</p>
      <pre className="max-h-72 overflow-auto rounded-md border bg-muted p-3 text-xs">{JSON.stringify(submitted, null, 2)}</pre>
    </form>
  )
}
```

## Installation

**CLI**

```bash
npx react-thaizip add address-form-field
```

The CLI picks files based on the style `init` recorded: projects with a shadcn/ui `components.json` get files composed from shadcn primitives (Base UI, Radix, or React Aria); everything else gets the vanilla files.

**Manual — Vanilla**

1. Install the dependencies: `npm install clsx tailwind-merge thaizip @base-ui/react react-hook-form`
1. Copy these files into your project:
   - `lib/utils.ts` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/lib/utils.ts)
   - `hooks/use-thai-address-index.ts` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-index.ts)
   - `hooks/use-thai-address-cascade.ts` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-cascade.ts)
   - `components/thai-address-cascade-select.tsx` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/thai-address-cascade-select.tsx)
   - `components/thai-address-form-field.tsx` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/thai-address-form-field.tsx)
1. Update the `@/lib/...` and `@/hooks/...` imports to match where the files live in your project (the CLI does this for you).
1. The vanilla style also needs the design tokens in your global stylesheet — see [Customization](https://react-thai-zip.vercel.app/en/docs/guides/customization#tokens-inserted-by-init).

**Manual — shadcn/ui · Base UI**

1. Install the dependencies: `npm install clsx tailwind-merge thaizip react-hook-form`
1. Add the shadcn/ui primitives the component uses: `npx shadcn@latest add select label button input`
1. Copy these files into your project:
   - `lib/utils.ts` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/lib/utils.ts)
   - `hooks/use-thai-address-index.ts` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-index.ts)
   - `hooks/use-thai-address-cascade.ts` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-cascade.ts)
   - `components/thai-address-cascade-select.tsx` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/shadcn/base/thai-address-cascade-select.tsx)
   - `components/thai-address-form-field.tsx` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/shadcn/base/thai-address-form-field.tsx)
1. Update the `@/lib/...` and `@/hooks/...` imports to match where the files live in your project (the CLI does this for you).

**Manual — shadcn/ui · Radix**

1. Install the dependencies: `npm install clsx tailwind-merge thaizip react-hook-form`
1. Add the shadcn/ui primitives the component uses: `npx shadcn@latest add select label button input`
1. Copy these files into your project:
   - `lib/utils.ts` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/lib/utils.ts)
   - `hooks/use-thai-address-index.ts` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-index.ts)
   - `hooks/use-thai-address-cascade.ts` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-cascade.ts)
   - `components/thai-address-cascade-select.tsx` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/shadcn/radix/thai-address-cascade-select.tsx)
   - `components/thai-address-form-field.tsx` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/shadcn/radix/thai-address-form-field.tsx)
1. Update the `@/lib/...` and `@/hooks/...` imports to match where the files live in your project (the CLI does this for you).

**Manual — shadcn/ui · React Aria**

1. Install the dependencies: `npm install clsx tailwind-merge thaizip react-hook-form`
1. Add the shadcn/ui primitives the component uses: `npx shadcn@latest add select label button input`
1. Copy these files into your project:
   - `lib/utils.ts` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/lib/utils.ts)
   - `hooks/use-thai-address-index.ts` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-index.ts)
   - `hooks/use-thai-address-cascade.ts` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/hooks/use-thai-address-cascade.ts)
   - `components/thai-address-cascade-select.tsx` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/shadcn/aria/thai-address-cascade-select.tsx)
   - `components/thai-address-form-field.tsx` — [source](https://github.com/naay99999/react-thai-zip/blob/main/templates/react/ts/shadcn/aria/thai-address-form-field.tsx)
1. Update the `@/lib/...` and `@/hooks/...` imports to match where the files live in your project (the CLI does this for you).

## Usage

```tsx
import { useForm } from 'react-hook-form'
import type { ResolvedThaiAddress } from 'thaizip'
import { ThaiAddressFormField } from '@/components/thai-address-form-field'
```

```tsx
const { control, handleSubmit } = useForm<{ address: ResolvedThaiAddress | null }>({
  defaultValues: { address: null },
})
```

```tsx
<ThaiAddressFormField control={control} name="address" />
```

The `name` you pass (`"address"` above) must have type `ResolvedThaiAddress | null` in the form's value type. react-hook-form's own `handleSubmit` reads the value directly — this component renders no hidden inputs.

## Examples

### Thai input

Without `locale`, labels and options of the embedded cascade select are in Thai (the default `locale="th"`).

```tsx title="address-form-field-demo.tsx"
'use client'

import { useState } from 'react'
import { useForm } from 'react-hook-form'
import type { ResolvedThaiAddress } from 'thaizip'
import { ThaiAddressFormField } from '@/components/thai-address-form-field'

type FormValues = {
  address: ResolvedThaiAddress | null
}

export default function AddressFormFieldDemo() {
  const { control, handleSubmit } = useForm<FormValues>({ defaultValues: { address: null } })
  const [submitted, setSubmitted] = useState<ResolvedThaiAddress | null>(null)

  return (
    <form className="flex flex-col gap-3" onSubmit={handleSubmit((data) => setSubmitted(data.address))}>
      <ThaiAddressFormField
        control={control}
        name="address"
      />
      <div>
        <button type="submit" className="rounded-md border border-input bg-background px-3 py-1.5 text-sm hover:bg-accent">
          ส่งฟอร์ม
        </button>
      </div>
      <p className="text-sm font-medium">ค่าที่ส่ง</p>
      <pre className="max-h-72 overflow-auto rounded-md border bg-muted p-3 text-xs">{JSON.stringify(submitted, null, 2)}</pre>
    </form>
  )
}
```

### Validation

Pass `rules` the same way you would to react-hook-form's `Controller`/`useController`. This example requires a complete address before the form can submit; the error message renders as a `<p role="alert">` below the cascade select.

```tsx title="address-form-field-validation-en.tsx"
'use client'

import { useState } from 'react'
import { useForm } from 'react-hook-form'
import type { ResolvedThaiAddress } from 'thaizip'
import { ThaiAddressFormField } from '@/components/thai-address-form-field'

type FormValues = {
  address: ResolvedThaiAddress | null
}

export default function AddressFormFieldValidationEn() {
  const { control, handleSubmit } = useForm<FormValues>({ defaultValues: { address: null } })
  const [submitted, setSubmitted] = useState<ResolvedThaiAddress | null>(null)

  return (
    <form className="flex flex-col gap-3" onSubmit={handleSubmit((data) => setSubmitted(data.address))}>
      <ThaiAddressFormField
        control={control}
        name="address"
        locale="en"
        rules={{ validate: (value) => value !== null || 'Please select a complete address.' }}
      />
      <div>
        <button type="submit" className="rounded-md border border-input bg-background px-3 py-1.5 text-sm hover:bg-accent">
          Submit
        </button>
      </div>
      <p className="text-sm font-medium">Submitted value</p>
      <pre className="max-h-72 overflow-auto rounded-md border bg-muted p-3 text-xs">{JSON.stringify(submitted, null, 2)}</pre>
    </form>
  )
}
```

### Disabled

Pass `disabled` to disable every select in the embedded cascade.

```tsx title="address-form-field-disabled.tsx"
'use client'

import { useForm } from 'react-hook-form'
import type { ResolvedThaiAddress } from 'thaizip'
import { ThaiAddressFormField } from '@/components/thai-address-form-field'

type FormValues = {
  address: ResolvedThaiAddress | null
}

export default function AddressFormFieldDisabled() {
  const { control } = useForm<FormValues>({ defaultValues: { address: null } })

  return <ThaiAddressFormField control={control} name="address" disabled />
}
```

### Form

The address field alongside an ordinary `register`ed input in the same form: both validate on submit, and `handleSubmit` receives every value at once.

```tsx title="address-form-field-full-en.tsx"
'use client'

import { useState } from 'react'
import { useForm } from 'react-hook-form'
import type { ResolvedThaiAddress } from 'thaizip'
import { ThaiAddressFormField } from '@/components/thai-address-form-field'

type FormValues = {
  name: string
  address: ResolvedThaiAddress | null
}

export default function AddressFormFieldFullEn() {
  const { control, register, handleSubmit, formState: { errors } } = useForm<FormValues>({
    defaultValues: { name: '', address: null },
  })
  const [submitted, setSubmitted] = useState<FormValues | null>(null)

  return (
    <form className="flex flex-col gap-4" onSubmit={handleSubmit(setSubmitted)}>
      <label className="flex flex-col gap-1.5 text-sm font-medium">
        Full name
        <input
          {...register('name', { required: 'Please enter your name.' })}
          aria-invalid={errors.name ? true : undefined}
          className="h-9 rounded-md border border-input bg-background px-3 text-sm"
        />
        {errors.name && <span role="alert" className="text-sm font-normal text-destructive">{errors.name.message}</span>}
      </label>
      <ThaiAddressFormField
        control={control}
        name="address"
        locale="en"
        rules={{ validate: (value) => value !== null || 'Please select a complete address.' }}
      />
      <div>
        <button type="submit" className="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90">
          Sign up
        </button>
      </div>
      <p className="text-sm font-medium">Submitted value</p>
      <pre className="max-h-72 overflow-auto rounded-md border bg-muted p-3 text-xs">{JSON.stringify(submitted, null, 2)}</pre>
    </form>
  )
}
```

## API Reference

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `control` | `Control<TFieldValues>` | — | **Required.** The `control` value from react-hook-form's `useForm()`. |
| `name` | `FieldPath<TFieldValues>` | — | **Required.** The form field's path; its value type must be `ResolvedThaiAddress \| null`. |
| `rules` | `Omit<RegisterOptions<…>, 'valueAsNumber' \| 'valueAsDate' \| 'setValueAs' \| 'disabled'>` | — | Forwarded to react-hook-form's `Controller` for validation. |
| `locale` | `'th' \| 'en'` | `'th'` | Label and option locale for the embedded cascade select. |
| `texts` | `Partial<ThaiAddressCascadeSelectTexts>` | — | Forwarded as the embedded cascade select's own `texts` prop. |
| `indexLoad` | `'mount' \| 'visible'` | `'mount'` | Forwarded to the embedded cascade's address-index loader. |
| `disabled` | `boolean` | — | Disables every select in the embedded cascade select. |
| `className` | `string` | — | The `<div>` wrapping the cascade select and its validation message. |
| `labelClassName` | `string` | — | The cascade select's labels. |
| `triggerClassName` | `string` | — | The cascade select's triggers and postal-code input. |
| `popupClassName` | `string` | — | The cascade select's option popups. |
| `itemClassName` | `string` | — | Each of the cascade select's option items. |
| `errorClassName` | `string` | — | The `role="alert"` validation message shown when validation fails. |

This component accepts no `value`/`onValueChange`/`ref`/`name` (for a hidden input) like the other components do — react-hook-form reads and writes the value through the `control`/`name` you pass instead.

## Related

See [Customization](https://react-thai-zip.vercel.app/en/docs/guides/customization) to change the presentation. If your form doesn't use react-hook-form, see [cascade select](https://react-thai-zip.vercel.app/en/docs/components/cascade-select) or [address form](https://react-thai-zip.vercel.app/en/docs/components/address-form), which use the usual `value`/`onValueChange`/hidden-input shape.

---

# Forms

> Submit Thai address values with browser forms or connect them to form-library state

Source: https://react-thai-zip.vercel.app/en/docs/guides/forms

The components generated by the CLI are regular browser form elements. They do not send or store data for you. When you provide `name`, the component renders hidden inputs that normal `FormData` and form submission can read.

## Submit with a browser form

Set `name="address"` to render these four fields:

```text
address-subdistrict
address-district
address-province
address-zipcode
```

After an address is selected, `new FormData(form)` has this payload shape. Area names are the selected Thai names, even when the UI is displayed in English:

```ts
{
  'address-subdistrict': 'คลองตันเหนือ',
  'address-district': 'วัฒนา',
  'address-province': 'กรุงเทพมหานคร',
  'address-zipcode': '10110',
}
```

This example prevents the form submission, reads `FormData`, and displays it on the page, so it needs no server:

```tsx title="form-autocomplete-en.tsx"
'use client'

import { useRef, useState, type FormEvent } from 'react'
import { ThaiAddressAutocomplete } from '@/components/thai-address-autocomplete'

export default function FormAutocompleteEn() {
  const ref = useRef<HTMLInputElement>(null)
  const [payload, setPayload] = useState<Record<string, FormDataEntryValue> | null>(null)

  function handleSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault()
    setPayload(Object.fromEntries(new FormData(event.currentTarget)))
  }

  return (
    <form className="flex flex-col gap-3" onSubmit={handleSubmit}>
      <ThaiAddressAutocomplete
        ref={ref}
        name="address"
        required
        locale="en"
      />
      <div className="flex gap-2">
        <button type="button" className="rounded-md border border-input bg-background px-3 py-1.5 text-sm hover:bg-accent" onClick={() => ref.current?.focus()}>
          Focus address
        </button>
        <button type="submit" className="rounded-md border border-input bg-background px-3 py-1.5 text-sm hover:bg-accent">
          Submit
        </button>
      </div>
      <p className="text-sm font-medium">Submitted FormData</p>
      <pre className="max-h-72 overflow-auto rounded-md border bg-muted p-3 text-xs">{JSON.stringify(payload, null, 2)}</pre>
    </form>
  )
}
```

```tsx title="form-cascade-select-en.tsx"
'use client'

import { useRef, useState, type FormEvent } from 'react'
import { ThaiAddressCascadeSelect } from '@/components/thai-address-cascade-select'

export default function FormCascadeSelectEn() {
  const ref = useRef<HTMLButtonElement>(null)
  const [payload, setPayload] = useState<Record<string, FormDataEntryValue> | null>(null)

  function handleSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault()
    setPayload(Object.fromEntries(new FormData(event.currentTarget)))
  }

  return (
    <form className="flex flex-col gap-3" onSubmit={handleSubmit}>
      <ThaiAddressCascadeSelect
        ref={ref}
        name="address"
        required
        locale="en"
      />
      <div className="flex gap-2">
        <button type="button" className="rounded-md border border-input bg-background px-3 py-1.5 text-sm hover:bg-accent" onClick={() => ref.current?.focus()}>
          Focus address
        </button>
        <button type="submit" className="rounded-md border border-input bg-background px-3 py-1.5 text-sm hover:bg-accent">
          Submit
        </button>
      </div>
      <p className="text-sm font-medium">Submitted FormData</p>
      <pre className="max-h-72 overflow-auto rounded-md border bg-muted p-3 text-xs">{JSON.stringify(payload, null, 2)}</pre>
    </form>
  )
}
```

Autocomplete and cascade select render the same field names. If you pass `disabled`, all four hidden inputs are disabled too and are omitted from `FormData`.

## Use a form library

Keep `ResolvedThaiAddress | null` in your form state or library controller, then pass it back through `value`. For autocomplete, this controlled pattern gives validation, resets, and submission one source of truth.

```tsx
const [address, setAddress] = useState<ResolvedThaiAddress | null>(null)

<ThaiAddressAutocomplete
  name="address"
  value={address}
  onValueChange={setAddress}
/>
```

For `ThaiAddressCascadeSelect`, use the form value for a complete resolved address only. The cascade manages an in-progress province/district selection internally and does not emit a resolved value until all three levels are selected. Passing `value={null}` clears an existing complete selection but intentionally preserves an in-progress partial selection, so form state is not the sole source of truth for that partial UI.

On a normal form reset, pass `null` to clear the saved complete address. If the reset must also discard an in-progress partial cascade selection, deliberately remount the cascade by incrementing a reset key:

```tsx
<ThaiAddressCascadeSelect key={resetVersion} name="address" value={address} onValueChange={setAddress} />
```

## Validation and accessibility

- `required` is applied to the autocomplete input or all three cascade-select controls.
- `aria-invalid` communicates an invalid state: autocomplete applies it to its input and cascade select applies it to all three triggers.
- Autocomplete's `onBlur` runs when its input loses focus; cascade select's runs when the province trigger loses focus.
- The autocomplete `ref` is an `HTMLInputElement`; the cascade-select `ref` is the province-trigger button, so either can be focused by your form logic.

For example, pass `aria-invalid={Boolean(error)}` from form validation and use `ref.current?.focus()` to return someone to the primary control that needs attention.

## Related guides

See [autocomplete](https://react-thai-zip.vercel.app/en/docs/components/autocomplete) for free-text search, [cascade select](https://react-thai-zip.vercel.app/en/docs/components/cascade-select) for ordered selection, and [Customization](https://react-thai-zip.vercel.app/en/docs/guides/customization) for text and class slots.

---

# Customization

> Change locale, text, class slots, and design tokens in scaffolded components

Source: https://react-thai-zip.vercel.app/en/docs/guides/customization

The files created by `react-thaizip add` are source files in your project. Start with props and Tailwind classes, then edit the scaffolded source when you need deeper behavior or markup changes.

## Locale and text

Pass `locale="en"` for English labels, placeholders, and options. `texts` accepts a partial override, so every omitted key keeps that locale's default text.

```tsx
<ThaiAddressAutocomplete
  locale="en"
  texts={{
    placeholder: 'Search delivery address',
    emptyText: 'No delivery area found',
  }}
/>
```

Autocomplete supports `placeholder`, `clearAriaLabel`, `loadingText`, `errorText`, `retryLabel`, and `emptyText` in `texts`. Cascade select supports `provinceLabel`, `districtLabel`, `subdistrictLabel`, `zipLabel`, its three placeholders, plus `loadingText`, `errorText`, and `retryLabel`.

## Autocomplete class slots

| Prop | Applied to |
| --- | --- |
| `className` | Root wrapper |
| `inputClassName` | Search input |
| `popupClassName` | Results popup |
| `itemClassName` | Each result item |

```tsx
<ThaiAddressAutocomplete
  inputClassName="border-sky-500 focus-visible:ring-sky-500"
  popupClassName="border-sky-200 shadow-lg"
/>
```

## Cascade-select class slots

| Prop | Applied to |
| --- | --- |
| `className` | Root grid wrapper |
| `labelClassName` | Every field label |
| `triggerClassName` | Select triggers and the postal-code input |
| `popupClassName` | Option popups |
| `itemClassName` | Each option item |

Use `triggerClassName` when the three triggers and postal-code input need one treatment, or `labelClassName` to style the whole label set.

## Tokens inserted by `init`

When `init` inserts its token block into the global stylesheet, the components use CSS custom properties including `--background`, `--foreground`, `--popover`, `--primary`, `--muted`, `--accent`, `--destructive`, `--border`, `--input`, `--ring`, and `--radius`.

This is an example from the Tailwind v4 block:

```css
:root {
  --background: oklch(1 0 0);
  --foreground: oklch(0.145 0 0);
  --popover: oklch(1 0 0);
  --primary: oklch(0.205 0 0);
  --muted: oklch(0.97 0 0);
  --accent: oklch(0.97 0 0);
  --destructive: oklch(0.577 0.245 27.325);
  --border: oklch(0.922 0 0);
  --input: oklch(0.922 0 0);
  --ring: oklch(0.708 0 0);
  --radius: 0.625rem;
}
```

In the same block, `.dark` overrides those color properties, while `--radius` remains shared from `:root` unless you override it explicitly. Edit the values for the theme you want in the stylesheet to change the scaffolded components' colors.

## Tailwind v4

For Tailwind v4, `init` writes `@theme inline` in its token block and automatically maps the CSS properties to Tailwind theme values:

```css
@theme inline {
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  --color-popover: var(--popover);
  --color-primary: var(--primary);
  --color-muted: var(--muted);
  --color-accent: var(--accent);
  --color-destructive: var(--destructive);
  --color-border: var(--border);
  --color-input: var(--input);
  --color-ring: var(--ring);
  --radius-lg: var(--radius);
  --radius-md: calc(var(--radius) - 2px);
  --radius-sm: calc(var(--radius) - 4px);
}
```

That makes classes such as `bg-background`, `border-input`, and `focus-visible:ring-ring` available once `init` has written the block.

## Tailwind v3

Tailwind v3 has no `@theme inline`, so `init` prints this snippet for you to add under `theme.extend` in `tailwind.config.{js,ts}`:

```ts
colors: {
  background: 'hsl(var(--background))',
  foreground: 'hsl(var(--foreground))',
  popover: { DEFAULT: 'hsl(var(--popover))', foreground: 'hsl(var(--popover-foreground))' },
  primary: { DEFAULT: 'hsl(var(--primary))', foreground: 'hsl(var(--primary-foreground))' },
  muted: { DEFAULT: 'hsl(var(--muted))', foreground: 'hsl(var(--muted-foreground))' },
  accent: { DEFAULT: 'hsl(var(--accent))', foreground: 'hsl(var(--accent-foreground))' },
  destructive: { DEFAULT: 'hsl(var(--destructive))', foreground: 'hsl(var(--destructive-foreground))' },
  border: 'hsl(var(--border))',
  input: 'hsl(var(--input))',
  ring: 'hsl(var(--ring))',
},
borderRadius: { lg: 'var(--radius)', md: 'calc(var(--radius) - 2px)', sm: 'calc(var(--radius) - 4px)' },
```

This manual step is required in Tailwind v3 before the components' token classes can resolve. If those classes still have no styles, follow the [Tailwind v3 troubleshooting steps](https://react-thai-zip.vercel.app/en/docs/troubleshooting#tailwind-v3-token-styles-are-missing).

## Go deeper in the scaffolded source

To change layout, add a slot, or alter behavior, edit the component file that `add` generated in your project. Keep the CLI-generated `utils` and hook imports, then adapt the markup and Tailwind classes to your design system.

## Related guides

See [Forms](https://react-thai-zip.vercel.app/en/docs/guides/forms) for `name`, hidden fields, and validation semantics.

---

# Use react-thaizip with AI

> Help an AI agent scaffold and compose Thai address components from the real project source

Source: https://react-thai-zip.vercel.app/en/docs/guides/using-with-ai

Have the agent use the CLI to add components to your React or Next.js project, then build the form by importing those generated files. The components become source files in your project; they are not runtime exports from the `react-thaizip` package.

## Choose a component

| Task | `add` target | Component |
| --- | --- | --- |
| Search by address text or postal code | `autocomplete` | `ThaiAddressAutocomplete` |
| Select province → district → sub-district | `cascade-select` | `ThaiAddressCascadeSelect` |
| Also capture house number and street details | `address-form` | `ThaiAddressForm` |
| Show a saved address | `address-display` | `ThaiAddressDisplay` |
| Use the cascade with react-hook-form | `address-form-field` | `ThaiAddressFormField` |

Read each component's behavior and props in the Components section before choosing, particularly `ResolvedThaiAddress | null`, form hidden inputs, and resets during a partial cascade selection.

## Set up the target project

Run these commands from the root of a React or Next.js project with Tailwind CSS v3 or v4 already installed:

```bash
npx react-thaizip init
npx react-thaizip add autocomplete
```

`init` writes `thaizip.config.json` v4 and selects a vanilla or shadcn/ui template from the project's `components.json`. `add` installs dependencies and prints the import path for the generated file. Use that actual path: the destination and `.tsx`/`.jsx` extension depend on the project. Avoid `--overwrite` until you have reviewed any source you previously edited.

## Prompt for a coding agent

Copy this into an agent that can access the target project, replacing the task in brackets:

```text
Build [a checkout address form] in this project using react-thaizip.

1. Inspect package.json, Tailwind, components.json, and thaizip.config.json first.
2. If the project is not configured yet, run npx react-thaizip init from its root.
3. Choose the appropriate component and run npx react-thaizip add <target>.
4. Use the import path printed by the CLI. Import the component from the scaffolded project file.
5. Follow that component's documented API. For a form, handle null, validation, submit, and reset.
6. Do not recreate the scaffolded component implementation or overwrite edited files without reviewing the diff.
7. Run the project's typecheck/build, then report the files created and verification result.
```

## Markdown for AI

Every docs page has a Markdown version that already includes the source of every example and the manual install steps. Give it to an agent that can read the web, along with the task:

| URL | Content |
| --- | --- |
| [`/llms.txt`](https://react-thai-zip.vercel.app/llms.txt) | Index of every page, linking to its `.md` file |
| [`/llms-full.txt`](https://react-thai-zip.vercel.app/llms-full.txt) | Every page in one file |
| [`/en/docs.md`](https://react-thai-zip.vercel.app/en/docs.md) | Any page: append `.md` to its URL, e.g. [`/en/docs/components/autocomplete.md`](https://react-thai-zip.vercel.app/en/docs/components/autocomplete.md) |
| [thaizip `/llms.txt`](https://naay99999.github.io/thai-zip/llms.txt) | The lower-level address search API the components use |

Each page also has a Copy Markdown button at the top for copying just that page.

## Use the generated file

This example uses the `@/components/...` alias like the rest of these docs. Use the import path printed by `add` if your project has no such alias or uses a different path:

```tsx
'use client'

import { useState } from 'react'
import type { ResolvedThaiAddress } from 'thaizip'
import { ThaiAddressAutocomplete } from '@/components/thai-address-autocomplete'

export function AddressField() {
  const [address, setAddress] = useState<ResolvedThaiAddress | null>(null)

  return (
    <ThaiAddressAutocomplete
      name="address"
      value={address}
      onValueChange={setAddress}
    />
  )
}
```

`onValueChange` receives `null` when the selected address is cleared. With `name="address"`, the component renders hidden inputs named `address-subdistrict`, `address-district`, `address-province`, and `address-zipcode`. See [Forms](https://react-thai-zip.vercel.app/en/docs/guides/forms) for submission examples.

## Check the agent's result

- The component came from `add`, and its import points to a real file in the project.
- Installed `thaizip` is at least `0.7.5`, with the dependencies and shadcn primitives requested by the CLI.
- The form handles `null`, validation, and reset; its hidden inputs or react-hook-form value match the chosen submission method.
- Typecheck/build runs, and at least one real address selection has been tried.

For command failures, see [Troubleshooting](https://react-thai-zip.vercel.app/en/docs/troubleshooting), the [CLI reference](https://react-thai-zip.vercel.app/en/docs/reference/cli), and [Configuration](https://react-thai-zip.vercel.app/en/docs/reference/config).

---

# CLI reference

> react-thaizip command syntax, flags, aliases, and component selection

Source: https://react-thai-zip.vercel.app/en/docs/reference/cli

Run the CLI through `npx react-thaizip` from the root of the React or Next.js project you want to scaffold.

## Command syntax

```text
react-thaizip init [--yes]
react-thaizip add [component...] [--yes] [--overwrite]
react-thaizip --help
react-thaizip --version
react-thaizip init --help
react-thaizip add --help
```

Prefix a command with `npx` when running it, for example `npx react-thaizip add autocomplete`.

## `init`

```npm
npx react-thaizip init
```

`init` performs these steps in order:

1. Detects the package manager, project structure, TypeScript, shadcn/ui style, and Tailwind CSS v3 or v4 (which must already be installed), then prints a detection summary before asking anything.
2. Lets you confirm or change `componentDir` unless `--yes` is present.
3. Adds design tokens to a global stylesheet, if one was found.
4. Offers to install `thaizip` when it is missing.
5. Writes `thaizip.config.json`.
6. If any manual follow-up steps are needed (the Tailwind v3 `theme.extend` snippet, or the design-token block when no global CSS was found), prints all of them together at the very end, under `=== Manual steps required ===`.
7. Prints the next step: `Next: run \`npx react-thaizip add autocomplete\` to add your first component.`

Sample output at the start of a run:

```text
Detected project settings:
  Components directory: app/components
  Lib directory: lib
  Hooks directory: hooks
  Package manager: npm
  Language: TypeScript
  Tailwind: v4 (app/globals.css)
  Component style: vanilla Tailwind (no components.json found)
```

Tailwind is always detected before the `componentDir` prompt, so a project without Tailwind installed stops immediately, before any question is asked.

See the [configuration reference](https://react-thai-zip.vercel.app/en/docs/reference/config) for the file shape and detection rules.

## `add`

Add one component:

```npm
npx react-thaizip add autocomplete
```

Add multiple targets by separating their names with spaces:

```npm
npx react-thaizip add autocomplete cascade-select
```

With no targets, `add` opens an interactive multiselect containing all five public components:

```npm
npx react-thaizip add
```

### Public names and aliases

Name matching is case-insensitive.

| Component | Accepted names and aliases |
| --- | --- |
| `ThaiAddressAutocomplete` | `autocomplete`, `thai-address-autocomplete`, `ThaiAddressAutocomplete` |
| `ThaiAddressCascadeSelect` | `cascade`, `cascade-select`, `thai-address-cascade-select`, `ThaiAddressCascadeSelect` |
| `ThaiAddressForm` | `address-form`, `thai-address-form`, `ThaiAddressForm` |
| `ThaiAddressDisplay` | `address-display`, `thai-address-display`, `ThaiAddressDisplay` |
| `ThaiAddressFormField` | `address-form-field`, `thai-address-form-field`, `ThaiAddressFormField` |

`add` pulls in each target's shared files automatically: all components use `utils`; address-index consumers also use the index hook, and cascade-based components use the cascade hook and select. These internal dependencies are not shown in the multiselect or `--help`, and you do not need to name them.

Before writing files, `add` checks that an existing `thaizip` is at least `0.7.5`, offers to install missing dependencies, and writes files to the directories in `thaizip.config.json`. The selected component style comes from `init`: vanilla interactive components use Base UI; supported shadcn/ui projects use their Base, Radix, or React Aria primitives, with missing primitives installed by the shadcn CLI.

## Flags

| Flag | Effect |
| --- | --- |
| `--yes`, `-y` | Skips confirmation prompts and uses each prompt's default answer. |
| `--overwrite` | Replaces existing component files without asking, and unlocks replacing `lib`/hook files that are otherwise protected. Applies only to `add`. |
| `--help`, `-h` | Prints help; place it after `init` or `add` for command-scoped help. |
| `--version`, `-v` | Prints the CLI version. |

Recognized flags can appear anywhere in the argument list. If help and version are both present, **help always wins**, and neither `init` nor `add` runs.

`--yes` does not mean “overwrite everything.” The default answer to component and config overwrite prompts is no, so existing files remain skipped. Use `--overwrite` separately when you intentionally want to replace a component. Existing hook and `lib` files stay protected by default even with `--yes`, since you're expected to hand-edit them; only an explicit `--overwrite` replaces them.

An unknown flag exits with an error before the command runs. See [Troubleshooting](https://react-thai-zip.vercel.app/en/docs/troubleshooting) for installation and existing-file recovery.

---

# Configuration reference

> thaizip.config.json v4, component styles, and project detection rules

Source: https://react-thai-zip.vercel.app/en/docs/reference/config

`npx react-thaizip init` writes `thaizip.config.json` at the project root. The `add` command reads it to locate component and shared-file destinations.

## v4 shape

This example shows every v4 field for a project without a supported shadcn/ui configuration. Paths and the recorded CLI version depend on your project.

```json
{
  "typescript": true,
  "componentDir": "app/components",
  "libDir": "lib",
  "hooksDir": "hooks",
  "packageManager": "npm",
  "tailwind": {
    "version": 4,
    "css": "app/globals.css"
  },
  "style": "vanilla",
  "shadcnBase": "",
  "shadcnUiAlias": "",
  "shadcnUiDir": "",
  "registryVersion": "0.3.7"
}
```

| Field | Accepted value | Meaning |
| --- | --- | --- |
| `typescript` | `true` or `false` | Auto-detected by `init` from the presence of `tsconfig.json` (no prompt). Determines whether `add` writes `.tsx`/`.ts` output as authored (`true`) or strips TypeScript syntax and writes `.jsx`/`.js` output instead (`false`). |
| `componentDir` | non-empty string | Component destination relative to the project root. |
| `libDir` | non-empty string | Destination for `utils.ts`; `init` defaults to `lib`. |
| `hooksDir` | non-empty string | Destination for shared hooks; defaults to `hooks`. |
| `packageManager` | `npm`, `yarn`, `pnpm`, `bun` | Package manager used by `add` for dependency installation. |
| `tailwind.version` | `3` or `4` | Detected Tailwind major version. |
| `tailwind.css` | string | Project-relative global CSS path, or `""` when no file was found. |
| `style` | `vanilla` or `shadcn` | Selects the authored Tailwind/Base UI templates or templates composed from the project's shadcn/ui primitives. Auto-detected by `init`. |
| `shadcnBase` | `""`, `base`, `radix`, or `aria` | `""` with `vanilla`; otherwise the component library behind the project's shadcn/ui primitives. |
| `shadcnUiAlias` | string | Import prefix from `components.json` `aliases.ui`; `""` with `vanilla`. |
| `shadcnUiDir` | string | Project-relative directory resolved from that alias; `""` with `vanilla`. |
| `registryVersion` | non-empty string | CLI version recorded the last time files were written; registry metadata, not the `thaizip` dependency version. |

Every path is resolved from the working directory where the CLI runs. Run `init` and `add` from the same project root that contains the config file.

`registryVersion` is more than bookkeeping: `add` uses it to detect stale protected `lib`/hook files. When `add` skips one of those files because it already exists (protected), and the recorded `registryVersion` is older than the running CLI's own version, it prints a warning that the file may be stale along with the exact command to refresh it, for example `npx react-thaizip add utils --overwrite`. Whenever `add` actually writes at least one file — component, `lib`, or hook — it automatically bumps `registryVersion` in `thaizip.config.json` to the running CLI's version.

## Component-style detection

`init` reads `components.json` when present. A `style` beginning with `base-`, `radix-`, or `aria-` selects the corresponding shadcn/ui template; legacy `default` and `new-york` select Radix. An absent or unrecognized style selects `vanilla`. For a recognized shadcn/ui project, `add` installs any missing UI primitives through the shadcn CLI. Vanilla interactive components use Base UI directly. `ThaiAddressDisplay` needs no UI primitives.

The selected `style` and `shadcnBase` are saved in the config. To change them after `init`, edit the config consistently and rerun `add` with `--overwrite` only after reviewing the files it would replace.

## Project-structure detection

`init` chooses the initial `componentDir` in this order, checking from the project root:

| Condition | Component directory |
| --- | --- |
| `app/` exists | `app/components/` |
| `app/` does not exist and `pages/` exists | `components/` |
| Neither exists | `src/components/` |

If both `app/` and `pages/` exist, `app/components/` wins. Without `--yes`, you can change the detected destination at the prompt before the config is written.

## Package-manager detection

The CLI uses the first lockfile found in this order:

1. `bun.lock` or `bun.lockb` → `bun`
2. `pnpm-lock.yaml` → `pnpm`
3. `yarn.lock` → `yarn`
4. `package-lock.json` → `npm`
5. No recognized lockfile → `npm`

## Tailwind detection

Tailwind is a prerequisite; `init` does not install it. Detection proceeds in this order:

1. Look for `@import "tailwindcss"` or its single-quoted form in a known global stylesheet. A match means Tailwind v4 with that CSS path.
2. If the `tailwindcss` dependency declares major version 4 or newer, use v4 and store `tailwind.css` as `""` when no stylesheet was found.
3. Look for `tailwind.config.ts`, `.js`, `.cjs`, or `.mjs`. A match means v3; the CLI then looks for a known global stylesheet containing `@tailwind`.
4. If the dependency declares major version 3, use v3 and store an empty CSS path when no stylesheet was found.
5. If none match, `init` stops and points you to Tailwind installation instructions.

The global CSS candidates, in order, are `app/globals.css`, `src/app/globals.css`, `styles/globals.css`, `src/styles/globals.css`, `src/index.css`, `src/App.css`, and `app/global.css`.

## File writes and import paths

Component templates are authored with `@/lib/*` and `@/hooks/*` imports so they can typecheck in the registry. Whenever `add` copies a component file, including an overwrite, it rewrites those specifiers to extensionless relative paths from `componentDir` to the configured `libDir` and `hooksDir`.

For example, with `componentDir` set to `app/components`, `libDir` to `lib`, and `hooksDir` to `hooks`, imports point out of `app/components` to those two directories. The target project does not need an `@` alias for generated component imports.

Existing `lib` and hook files are protected by default, even with `--yes`, and are replaced only with an explicit `--overwrite`. Existing component files are skipped by default and replaced only after confirmation or with `--overwrite`. Import rewriting therefore runs only when a component is actually copied.

## Legacy configuration

When `add` reads a recognized v1, v2, or v3 config, it migrates it in place to v4. The v1 migration fills in `libDir`, `hooksDir`, and Tailwind metadata (using Tailwind v4 with an empty CSS path if detection fails), while preserving `typescript: false` when present. v1/v2 configs become `vanilla`; an existing v3 `shadcn` config gains `shadcnBase: "base"`, since v3 only recognized Base UI-backed shadcn styles. Migration does not re-detect the project's current shadcn style. If the resulting config is invalid, rerun `npx react-thaizip init` to regenerate it.

If paths or metadata do not match the project, follow the recovery steps in [Troubleshooting](https://react-thai-zip.vercel.app/en/docs/troubleshooting).

---

# Troubleshooting

> Causes and exact recovery steps for init, add, Tailwind, dependencies, and existing files

Source: https://react-thai-zip.vercel.app/en/docs/troubleshooting

Run commands from the project root containing `package.json` and `thaizip.config.json`, except when a section is creating that config for the first time.

## `init` cannot detect Tailwind

**Problem:** `init` says Tailwind CSS is required and stops without writing a config.

**Cause:** The project has no Tailwind v3/v4 signal the CLI recognizes. Tailwind is a prerequisite; react-thaizip does not install it.

**Recovery:** Install and configure Tailwind for your framework so the dependency and CSS/config entry points are present, then run:

```npm
npx react-thaizip init
```

## No global CSS file was found

**Problem:** `init` prints `No global CSS file was found`, followed by a design-token block.

**Cause:** It detected the Tailwind version from a dependency or config but could not locate a supported stylesheet.

**Recovery:** Copy the entire printed block into the global stylesheet your app actually loads. If necessary, record its correct project-relative path in `tailwind.css` in [`thaizip.config.json`](https://react-thai-zip.vercel.app/en/docs/reference/config).

## Tailwind v3 token styles are missing

**Problem:** The component has classes such as `bg-background` or `border-input`, but the expected color or style is absent.

**Cause:** Tailwind v3 has no `@theme inline`. `init` prints the required `theme.extend` mapping but does not edit the Tailwind config automatically.

**Recovery:** Add the `theme.extend` snippet printed by `init` under `theme.extend` in `tailwind.config.js` or `tailwind.config.ts`, then restart a running development server.

## `thaizip` is older than `0.7.5`

**Problem:** `add` reports the version it found, requires `thaizip >=0.7.5`, and stops before scaffolding.

**Cause:** The templates use the cascade/enumeration API and bilingual labels introduced in `0.7.0`, plus the synchronous index-cache API available from `0.7.5`.

**Recovery:** Upgrade the package, then rerun the original `add` command:

```npm
npm i thaizip@latest
npx react-thaizip add autocomplete
```

Use the equivalent add command for yarn, pnpm, or bun, and replace the second-line target with the one from your original command.

## Dependency installation failed

**Problem:** `init` or `add` prints `Failed to install` and a package list.

**Cause:** The package manager exited with an error, for example because of a registry, network, or lockfile problem. The CLI stops without writing components.

**Recovery:** Install the printed list with the project's package manager, then rerun the original command. For example, the full vanilla autocomplete dependency set with npm is:

```npm
npm i 'thaizip@^0.7.5' @base-ui/react clsx tailwind-merge
npx react-thaizip add autocomplete
```

If `init` failed while installing `thaizip`, install that package first and rerun `npx react-thaizip init`.

In a supported shadcn/ui project, `add` uses the project's primitives instead of installing `@base-ui/react` directly. Follow the dependency or shadcn CLI command printed by the failed run.

## A component file already exists

**Problem:** `add` asks whether to overwrite or reports that it skipped an existing component.

**Cause:** The safe default preserves your component source. `--yes` uses the default “no” answer, so it does not overwrite the file.

**Recovery:** Review the diff or back up your changes, then confirm the prompt or use this flag only when you intend to replace the file:

```npm
npx react-thaizip add autocomplete --overwrite
```

## A hook or `lib` file already exists

**Problem:** `add` prints `Skipped lib/utils.ts (protected; pass --overwrite to update it).`, or the equivalent for `hooks/use-thai-address-index.ts`, even with `--yes`.

**Cause:** This is intentional: shared files may contain your edits. These files stay protected by default, even with `--yes`, and unlike component files there is no confirmation prompt for them — only an explicit `--overwrite` replaces them.

**Recovery:** If the existing file is correct, do nothing. If you need a fresh copy, back up your changes first (it will be replaced without asking), then rerun the original command with `--overwrite`:

```npm
npx react-thaizip add utils --overwrite
```

Swap `utils` for `use-thai-address-index`, or for the name of the component that pulls in that file as a dependency, depending on which file you need to refresh.

## A scaffolded hook or `lib` file looks outdated after upgrading the CLI

**Problem:** After upgrading `react-thaizip` and rerunning `add`, the command skips `lib/utils.ts` or the hook and prints an extra line such as `lib/utils.ts predates the current registry (recorded v0.0.1, current v0.3.1) and may be stale. Run \`npx react-thaizip add utils --overwrite\` to refresh it.`

**Cause:** `thaizip.config.json` stores `registryVersion`, recorded the last time `add` actually wrote files. When a newer CLI encounters a protected `lib`/hook file and the recorded `registryVersion` is older than the version currently running, it warns that the file may be stale relative to the current templates. See [Configuration reference](https://react-thai-zip.vercel.app/en/docs/reference/config) for details on this field.

**Recovery:** Diff the file against the latest template if you want to check first, then run the exact command the message suggests to refresh it:

```npm
npx react-thaizip add utils --overwrite
```

Once the file is rewritten, `registryVersion` is automatically bumped to match the current CLI version, and the warning will not appear again until the next CLI upgrade.

## Import paths are unexpected

**Problem:** A scaffolded component imports `utils` or the hook from a relative path that does not fit the current layout.

**Cause:** `add` calculates imports from the working directory and the configured `componentDir`, `libDir`, and `hooksDir`. The command may have run from the wrong directory, or the config paths may not match the project.

**Recovery:** Inspect all three directories in `thaizip.config.json` and make sure they are relative to the project root. Then `cd` to that root and scaffold again. If the component already exists, intentionally pass `--overwrite` so the file and imports are regenerated:

```npm
npx react-thaizip add autocomplete --overwrite
```

Since `utils` and the hook are automatically pulled in as registry dependencies of the component, the command above also replaces any existing `lib`/hook files, not just the component file. Those files contain no config-relative paths, so they are unrelated to the import-path problem, but they get refreshed along the way.

## `thaizip.config.json` is missing

**Problem:** `add` asks `No thaizip.config.json found. Run init now?`

**Cause:** `init` has not run, the file was removed, or the command is running from a different directory.

**Recovery:** Accept the prompt so `add` runs `init` and continues, or run these commands yourself from the project root:

```npm
npx react-thaizip init
npx react-thaizip add autocomplete
```

See the [CLI reference](https://react-thai-zip.vercel.app/en/docs/reference/cli) for all commands and flags.
