> ## Documentation Index
> Fetch the complete documentation index at: https://docs.thunderphone.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Headless Hook

> Bangun UI suara yang sepenuhnya kustom dengan hook React useThunderPhone

Hook `useThunderPhone` memberi Anda kendali penuh atas antarmuka pengguna, sementara ThunderPhone mengelola sesi suara, perutean audio, dan status koneksi. Gunakan saat Anda menginginkan UI yang sepenuhnya kustom -- tombol, tata letak, animasi, dan branding Anda sendiri -- sementara ThunderPhone menangani semuanya di balik layar.

## Kapan Menggunakan Hook Headless

Komponen `ThunderPhoneWidget` bawaan mencakup sebagian besar kasus penggunaan, tetapi gunakan hook headless saat Anda memerlukan:

* UI panggilan yang sepenuhnya kustom dan sesuai dengan sistem desain aplikasi Anda
* Visualisasi yang responsif terhadap audio (bentuk gelombang, orb, indikator berdenyut) yang didorong oleh level audio real-time
* Alur panggilan kustom seperti formulir sebelum panggilan, survei setelah panggilan, atau chat inline bersama suara
* Integrasi ke dalam library komponen yang sudah ada (Material UI, Chakra, Radix, dll.)

***

## Instalasi

```bash theme={null}
npm install @thunderphone/widget
```

<Note>
  Hook headless **tidak** memerlukan impor `@thunderphone/widget/style.css` karena Anda menyediakan UI sendiri. Namun, Anda tetap harus menginstal paket `@thunderphone/widget` yang sama.
</Note>

***

## Penggunaan Dasar

```tsx theme={null}
import { useThunderPhone } from '@thunderphone/widget'

function CustomCallButton() {
  const phone = useThunderPhone({
    publishableKey: 'pk_live_your_publishable_key',
  })

  const handleClick = () => {
    if (phone.state === 'connected') {
      phone.disconnect()
    } else {
      phone.connect()
    }
  }

  return (
    <>
      <button onClick={handleClick} disabled={phone.state === 'connecting'}>
        {phone.state === 'connecting'
          ? 'Connecting...'
          : phone.state === 'connected'
            ? 'End call'
            : 'Start call'}
      </button>
      {phone.audio}
    </>
  )
}
```

<Warning>
  **Anda harus merender `phone.audio` di suatu tempat dalam pohon komponen Anda.** Ini adalah elemen React tak terlihat yang mengelola koneksi audio yang mendasarinya. Jika Anda mengabaikannya, tidak ada audio yang akan diputar dan sesi tidak akan berfungsi.
</Warning>

***

## Opsi

Teruskan opsi ini ke `useThunderPhone` melalui `UseThunderPhoneOptions`:

| Opsi             | Tipe                | Wajib | Default                             | Deskripsi                                                                                                                                                           |
| ---------------- | ------------------- | ----- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `publishableKey` | `string`            | Ya    | --                                  | Kunci API publik (`pk_live_...`). Agen ditentukan secara otomatis dari konfigurasi widget kunci tersebut.                                                           |
| `apiBase`        | `string`            | Tidak | `'https://api.thunderphone.com/v1'` | Penggantian URL dasar API.                                                                                                                                          |
| `language`       | `string`            | Tidak | --                                  | Penggantian bahasa per sesi -- kode bahasa atau lokal seperti `en`, `es`, atau `fr-FR`. Jika tidak ditetapkan, bahasa yang dikonfigurasi untuk agen akan digunakan. |
| `voice`          | `string`            | Tidak | --                                  | Penggantian suara per sesi -- nama suara seperti `maria`. Jika tidak ditetapkan, suara yang dikonfigurasi untuk agen akan digunakan.                                |
| `context`        | `string`            | Tidak | --                                  | Konteks faktual halaman atau situs per sesi yang diteruskan ke agen. Dipotong di sisi server hingga 12.000 karakter.                                                |
| `onConnect`      | `() => void`        | Tidak | --                                  | Dipanggil saat sesi suara terhubung.                                                                                                                                |
| `onDisconnect`   | `() => void`        | Tidak | --                                  | Dipanggil saat sesi berakhir.                                                                                                                                       |
| `onError`        | `(error) => void`   | Tidak | --                                  | Dipanggil saat terjadi kesalahan. Error memiliki field `error` (kode) dan `message`.                                                                                |
| `ringtone`       | `boolean \| string` | Tidak | `false`                             | Putar nada dering saat menghubungkan. `true` untuk nada dering default, atau string URL untuk audio kustom.                                                         |

<Note>
  Hook ini bersifat headless: hook ini **tidak** menerima prop tampilan `ThunderPhoneWidget` (`theme`, `primaryColor`, `title`, `position`, `className`). Meneruskannya akan menghasilkan error TypeScript -- tampilan sepenuhnya Anda yang membangunnya.
</Note>

***

## Nilai Kembalian

Hook mengembalikan objek `UseThunderPhoneReturn`:

| Properti        | Tipe                                                                 | Deskripsi                                                                                                                                                                                                                                                                                                                                                                                            |
| --------------- | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `state`         | `'idle' \| 'connecting' \| 'connected' \| 'disconnected' \| 'error'` | Status koneksi saat ini.                                                                                                                                                                                                                                                                                                                                                                             |
| `connect`       | `() => void`                                                         | Memulai sesi suara.                                                                                                                                                                                                                                                                                                                                                                                  |
| `disconnect`    | `() => void`                                                         | Mengakhiri sesi saat ini.                                                                                                                                                                                                                                                                                                                                                                            |
| `toggleMute`    | `() => void`                                                         | Mengaktifkan/menonaktifkan senyap mikrofon.                                                                                                                                                                                                                                                                                                                                                          |
| `isMuted`       | `boolean`                                                            | Apakah mikrofon saat ini disenyapkan.                                                                                                                                                                                                                                                                                                                                                                |
| `error`         | `string \| undefined`                                                | Pesan kesalahan saat status adalah `'error'`.                                                                                                                                                                                                                                                                                                                                                        |
| `agentName`     | `string \| undefined`                                                | Nama tampilan agen yang terhubung.                                                                                                                                                                                                                                                                                                                                                                   |
| `audioLevel`    | `number`                                                             | **Tidak digunakan lagi -- selalu `0`.** Placeholder statis yang dipertahankan untuk kompatibilitas mundur; nilainya tidak pernah diperbarui. Gunakan `audioLevelRef.current` sebagai gantinya.                                                                                                                                                                                                       |
| `audioLevelRef` | `React.RefObject<number>`                                            | Ref yang dapat diubah dan berisi level audio waktu nyata (0--1) -- yang lebih keras antara suara agen dan mikrofon pengunjung -- diperbarui pada setiap frame animasi, di luar siklus render React. Baca `audioLevelRef.current` di dalam loop `requestAnimationFrame` untuk animasi yang mulus tanpa tersendat, atau ambil sampelnya pada interval saat Anda memerlukan nilainya dalam state React. |
| `audio`         | `ReactNode`                                                          | Elemen tak terlihat yang menangani koneksi audio -- **harus dirender**.                                                                                                                                                                                                                                                                                                                              |

***

## UI Reaktif Audio

Ref `audioLevelRef` memberi Anda level audio pada laju frame tanpa memicu render ulang React, sehingga ideal untuk menggerakkan visualisasi bentuk gelombang yang mulus, orb berdenyut, atau animasi apa pun yang terkait dengan percakapan. Level mencerminkan sumber yang lebih keras: suara agen atau mikrofon pengunjung.

### Contoh Bentuk Gelombang

```tsx theme={null}
import { useRef, useEffect } from 'react'
import { useThunderPhone } from '@thunderphone/widget'

function WaveformCall() {
  const phone = useThunderPhone({
    publishableKey: 'pk_live_your_publishable_key',
  })
  const canvasRef = useRef<HTMLCanvasElement>(null)

  useEffect(() => {
    if (phone.state !== 'connected') return
    const canvas = canvasRef.current
    if (!canvas) return
    const ctx = canvas.getContext('2d')!

    let animId: number
    const draw = () => {
      const level = phone.audioLevelRef.current ?? 0
      ctx.clearRect(0, 0, canvas.width, canvas.height)

      // Draw bars that react to audio level
      const barCount = 24
      const barWidth = canvas.width / barCount
      for (let i = 0; i < barCount; i++) {
        const distance = Math.abs(i - barCount / 2) / (barCount / 2)
        const height = level * canvas.height * (1 - distance * 0.6)
        const y = (canvas.height - height) / 2
        ctx.fillStyle = '#0ea5e9'
        ctx.fillRect(i * barWidth + 1, y, barWidth - 2, height)
      }

      animId = requestAnimationFrame(draw)
    }
    animId = requestAnimationFrame(draw)
    return () => cancelAnimationFrame(animId)
  }, [phone.state, phone.audioLevelRef])

  return (
    <div>
      {phone.state === 'connected' && (
        <canvas ref={canvasRef} width={240} height={80} />
      )}
      <button
        onClick={phone.state === 'connected' ? phone.disconnect : phone.connect}
        disabled={phone.state === 'connecting'}
      >
        {phone.state === 'connected' ? 'End call' : 'Start call'}
      </button>
      {phone.audio}
    </div>
  )
}
```

### Contoh Orb Berdenyut

```tsx theme={null}
import { useRef, useEffect } from 'react'
import { useThunderPhone } from '@thunderphone/widget'

function PulsingOrb() {
  const phone = useThunderPhone({
    publishableKey: 'pk_live_your_publishable_key',
  })
  const orbRef = useRef<HTMLDivElement>(null)

  useEffect(() => {
    if (phone.state !== 'connected') return
    let animId: number
    const animate = () => {
      const level = phone.audioLevelRef.current ?? 0
      if (orbRef.current) {
        const scale = 1 + level * 0.5
        orbRef.current.style.transform = `scale(${scale})`
        orbRef.current.style.opacity = `${0.6 + level * 0.4}`
      }
      animId = requestAnimationFrame(animate)
    }
    animId = requestAnimationFrame(animate)
    return () => cancelAnimationFrame(animId)
  }, [phone.state, phone.audioLevelRef])

  return (
    <div style={{ textAlign: 'center' }}>
      <div
        ref={orbRef}
        style={{
          width: 80,
          height: 80,
          borderRadius: '50%',
          background: '#0ea5e9',
          margin: '20px auto',
          transition: 'transform 0.05s ease-out',
        }}
      />
      <button
        onClick={phone.state === 'connected' ? phone.disconnect : phone.connect}
        disabled={phone.state === 'connecting'}
      >
        {phone.state === 'connected' ? 'End call' : 'Call'}
      </button>
      {phone.audio}
    </div>
  )
}
```

### Contoh Indikator Berbicara

Untuk UI yang dirender React dan berubah sesuai volume -- seperti lencana "berbicara" berbasis ambang batas -- ambil sampel `audioLevelRef.current` pada interval tertentu dan simpan hasilnya dalam state:

```tsx theme={null}
import { useEffect, useState } from 'react'
import { useThunderPhone } from '@thunderphone/widget'

function SpeakingBadge() {
  const phone = useThunderPhone({
    publishableKey: 'pk_live_your_publishable_key',
  })
  const [speaking, setSpeaking] = useState(false)

  useEffect(() => {
    if (phone.state !== 'connected') {
      setSpeaking(false)
      return
    }
    const interval = setInterval(() => {
      setSpeaking((phone.audioLevelRef.current ?? 0) > 0.1)
    }, 100)
    return () => clearInterval(interval)
  }, [phone.state, phone.audioLevelRef])

  return (
    <div>
      {phone.state === 'connected' && (
        <span>{speaking ? 'Speaking' : 'Listening'}</span>
      )}
      <button onClick={phone.state === 'connected' ? phone.disconnect : phone.connect}>
        {phone.state === 'connected' ? 'End call' : 'Start call'}
      </button>
      {phone.audio}
    </div>
  )
}
```

<Warning>
  Selalu baca level dari `audioLevelRef.current`. Angka `audioLevel` pada objek pengembalian **sudah tidak digunakan lagi dan selalu bernilai `0`** -- logika apa pun yang dibangun di atasnya akan secara diam-diam membaca nol.
</Warning>

***

## Mesin Status

Properti `state` mengikuti siklus hidup ini:

```
idle --> connecting --> connected --> disconnected --> idle (after 1.5s)
                  \
                   --> error (stays until connect() is called again)
```

| Status         | Deskripsi                                                                                                                                                                             |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `idle`         | Tidak ada sesi aktif. Siap memanggil `connect()`.                                                                                                                                     |
| `connecting`   | Sesi sedang dibuat. Nonaktifkan tombol panggilan selama status ini.                                                                                                                   |
| `connected`    | Sesi suara aktif. Pengguna sedang berbicara dengan agen.                                                                                                                              |
| `disconnected` | Sesi telah berakhir dengan normal. Beralih kembali ke `idle` secara otomatis setelah 1,5 detik.                                                                                       |
| `error`        | Terjadi kesalahan. Periksa `phone.error` untuk pesannya. Status **tidak** akan dihapus dengan sendirinya -- memanggil `connect()` lagi akan memulai upaya baru dan mereset kesalahan. |

***

## Contoh

### Dengan Kontrol Bisukan

```tsx theme={null}
import { useThunderPhone } from '@thunderphone/widget'

function CallWithMute() {
  const phone = useThunderPhone({
    publishableKey: 'pk_live_your_publishable_key',
  })

  return (
    <div>
      {phone.state === 'connected' && (
        <div>
          <p>Talking to {phone.agentName ?? 'Agent'}</p>
          <button onClick={phone.toggleMute}>
            {phone.isMuted ? 'Unmute' : 'Mute'}
          </button>
          <button onClick={phone.disconnect}>End call</button>
        </div>
      )}

      {phone.state !== 'connected' && (
        <button
          onClick={phone.connect}
          disabled={phone.state === 'connecting'}
        >
          {phone.state === 'connecting' ? 'Connecting...' : 'Call support'}
        </button>
      )}

      {phone.state === 'error' && (
        <p style={{ color: 'red' }}>{phone.error}</p>
      )}

      {phone.audio}
    </div>
  )
}
```

### Dengan Nada Dering

Putar suara dering saat menghubungkan untuk menyimulasikan panggilan telepon:

```tsx theme={null}
import { useThunderPhone } from '@thunderphone/widget'

function PhoneCallButton() {
  const phone = useThunderPhone({
    publishableKey: 'pk_live_your_publishable_key',
    ringtone: true, // or a custom URL: 'https://example.com/ringtone.mp3'
  })

  return (
    <>
      <button
        onClick={phone.state === 'connected' ? phone.disconnect : phone.connect}
        disabled={phone.state === 'connecting'}
      >
        {phone.state === 'connecting'
          ? 'Ringing...'
          : phone.state === 'connected'
            ? 'Hang up'
            : 'Call'}
      </button>
      {phone.audio}
    </>
  )
}
```

Nada dering berulang selama status `connecting` dan memudar saat agen terhubung. Berikan `true` untuk nada dering bawaan, atau string URL untuk menggunakan file audio Anda sendiri.

### Dengan Callback Peristiwa

```tsx theme={null}
import { useThunderPhone } from '@thunderphone/widget'

function TrackedCallButton() {
  const phone = useThunderPhone({
    publishableKey: 'pk_live_your_publishable_key',
    onConnect: () => {
      analytics.track('call_started')
    },
    onDisconnect: () => {
      analytics.track('call_ended')
    },
    onError: (error) => {
      analytics.track('call_error', { code: error.error, message: error.message })
    },
  })

  return (
    <>
      <button
        onClick={phone.state === 'connected' ? phone.disconnect : phone.connect}
        disabled={phone.state === 'connecting'}
      >
        {phone.state === 'connected' ? 'Hang up' : 'Talk to AI'}
      </button>
      {phone.audio}
    </>
  )
}
```

### UI Kustom Penuh

```tsx theme={null}
import { useThunderPhone } from '@thunderphone/widget'

function FullCustomUI() {
  const phone = useThunderPhone({
    publishableKey: 'pk_live_your_publishable_key',
  })

  return (
    <div className="call-panel">
      <div className="call-status">
        {phone.state === 'idle' && <span>Ready</span>}
        {phone.state === 'connecting' && <span className="pulse">Connecting...</span>}
        {phone.state === 'connected' && (
          <span>On call with {phone.agentName}</span>
        )}
        {phone.state === 'error' && <span className="error">{phone.error}</span>}
      </div>

      <div className="call-controls">
        {phone.state === 'connected' ? (
          <>
            <button className="mute-btn" onClick={phone.toggleMute}>
              {phone.isMuted ? 'Unmute' : 'Mute'}
            </button>
            <button className="end-btn" onClick={phone.disconnect}>
              End
            </button>
          </>
        ) : (
          <button
            className="start-btn"
            onClick={phone.connect}
            disabled={phone.state === 'connecting'}
          >
            Start call
          </button>
        )}
      </div>

      {/* Required -- handles audio under the hood */}
      {phone.audio}
    </div>
  )
}
```

***

## Tips

<AccordionGroup>
  <Accordion title="Selalu render phone.audio">
    Elemen `phone.audio` tidak terlihat tetapi diperlukan. Tempatkan di mana saja dalam JSX Anda -- elemen ini tidak merender DOM yang terlihat, tetapi mengelola koneksi audio WebRTC secara internal.
  </Accordion>

  <Accordion title="Nonaktifkan tombol saat menghubungkan">
    Status `connecting` dapat berlangsung selama 1-3 detik. Nonaktifkan tombol panggilan selama status ini untuk mencegah upaya koneksi duplikat.
  </Accordion>

  <Accordion title="Tangani status error dengan baik">
    Saat statusnya `error`, tampilkan `phone.error` kepada pengguna dan biarkan tombol panggilan Anda tetap aktif. Hook tidak keluar dari status `error` dengan sendirinya -- memanggil `connect()` lagi memulai upaya baru dan menghapus error sebelumnya.
  </Accordion>

  <Accordion title="Gunakan callback untuk efek samping">
    Callback `onConnect`, `onDisconnect`, dan `onError` ideal untuk analitik, logging, atau memicu logika aplikasi lain tanpa melakukan polling pada status.
  </Accordion>

  <Accordion title="Baca level audio dari audioLevelRef">
    `audioLevelRef` adalah satu-satunya sumber level audio langsung. Baca `audioLevelRef.current` di dalam `requestAnimationFrame` untuk animasi mulus seperti bentuk gelombang (membaca ref tidak menyebabkan render ulang), atau ambil sampelnya pada interval tertentu dan simpan hasilnya dalam status untuk UI yang dirender React. Angka `audioLevel` sudah tidak digunakan lagi dan selalu `0` -- jangan membangun logika berdasarkan angka tersebut.
  </Accordion>
</AccordionGroup>
