diff --git a/dist/exports/client.d.ts b/dist/exports/client.d.ts
index 7a4c12e..1e66075 100644
--- a/dist/exports/client.d.ts
+++ b/dist/exports/client.d.ts
@@ -10,7 +10,8 @@ export { Analytics } from '../modules/analytics/client.js';
*/
export { ConsentProvider, CookieBanner, CookieButton, useConsent, useConsentContext, } from '../modules/consent/client.js';
export type { CookieBannerClassNames } from '../modules/consent/client.js';
-export { Turnstile } from '../modules/turnstile/client.js';
-export type { TurnstileProps } from '../modules/turnstile/client.js';
export { resolveFormMessage } from '../modules/notifications/resolveFormMessage.js';
export type { FormNotificationTexts } from '../modules/notifications/types.js';
+export { Turnstile } from '../modules/turnstile/client.js';
+export { TurnstileProvider, useTurnstile } from '../modules/turnstile/client.js';
+export type { TurnstileProps } from '../modules/turnstile/client.js';
diff --git a/dist/exports/client.js b/dist/exports/client.js
index 40e9411..e4b2dc0 100644
--- a/dist/exports/client.js
+++ b/dist/exports/client.js
@@ -9,7 +9,8 @@ export { Analytics } from '../modules/analytics/client.js';
* components. Kept separate from the main entry so server bundles don't pull in
* client-only code.
*/ export { ConsentProvider, CookieBanner, CookieButton, useConsent, useConsentContext } from '../modules/consent/client.js';
-export { Turnstile } from '../modules/turnstile/client.js';
export { resolveFormMessage } from '../modules/notifications/resolveFormMessage.js';
+export { Turnstile } from '../modules/turnstile/client.js';
+export { TurnstileProvider, useTurnstile } from '../modules/turnstile/client.js';
//# sourceMappingURL=client.js.map
\ No newline at end of file
diff --git a/dist/exports/client.js.map b/dist/exports/client.js.map
index 2ccf330..4a8cde4 100644
--- a/dist/exports/client.js.map
+++ b/dist/exports/client.js.map
@@ -1 +1 @@
-{"version":3,"sources":["../../src/exports/client.ts"],"sourcesContent":["'use client'\nexport { MaskedField } from '../globals/SiteIntegrations/components/MaskedField.js'\nexport { TestEmailButton } from '../globals/SiteIntegrations/components/TestEmailButton.js'\nexport { Analytics } from '../modules/analytics/client.js'\n/**\n * Entry point: ipal-kit/client\n *\n * Client-side ('use client') exports — React hooks, providers, and UI\n * components. Kept separate from the main entry so server bundles don't pull in\n * client-only code.\n */\nexport {\n ConsentProvider,\n CookieBanner,\n CookieButton,\n useConsent,\n useConsentContext,\n} from '../modules/consent/client.js'\nexport type { CookieBannerClassNames } from '../modules/consent/client.js'\nexport { Turnstile } from '../modules/turnstile/client.js'\nexport type { TurnstileProps } from '../modules/turnstile/client.js'\n\nexport { resolveFormMessage } from '../modules/notifications/resolveFormMessage.js'\nexport type { FormNotificationTexts } from '../modules/notifications/types.js'\n"],"names":["MaskedField","TestEmailButton","Analytics","ConsentProvider","CookieBanner","CookieButton","useConsent","useConsentContext","Turnstile","resolveFormMessage"],"mappings":"AAAA;AACA,SAASA,WAAW,QAAQ,wDAAuD;AACnF,SAASC,eAAe,QAAQ,4DAA2D;AAC3F,SAASC,SAAS,QAAQ,iCAAgC;AAC1D;;;;;;CAMC,GACD,SACEC,eAAe,EACfC,YAAY,EACZC,YAAY,EACZC,UAAU,EACVC,iBAAiB,QACZ,+BAA8B;AAErC,SAASC,SAAS,QAAQ,iCAAgC;AAG1D,SAASC,kBAAkB,QAAQ,iDAAgD"}
\ No newline at end of file
+{"version":3,"sources":["../../src/exports/client.ts"],"sourcesContent":["'use client'\nexport { MaskedField } from '../globals/SiteIntegrations/components/MaskedField.js'\nexport { TestEmailButton } from '../globals/SiteIntegrations/components/TestEmailButton.js'\nexport { Analytics } from '../modules/analytics/client.js'\n/**\n * Entry point: ipal-kit/client\n *\n * Client-side ('use client') exports — React hooks, providers, and UI\n * components. Kept separate from the main entry so server bundles don't pull in\n * client-only code.\n */\nexport {\n ConsentProvider,\n CookieBanner,\n CookieButton,\n useConsent,\n useConsentContext,\n} from '../modules/consent/client.js'\nexport type { CookieBannerClassNames } from '../modules/consent/client.js'\nexport { resolveFormMessage } from '../modules/notifications/resolveFormMessage.js'\nexport type { FormNotificationTexts } from '../modules/notifications/types.js'\nexport { Turnstile } from '../modules/turnstile/client.js'\nexport { TurnstileProvider, useTurnstile } from '../modules/turnstile/client.js'\nexport type { TurnstileProps } from '../modules/turnstile/client.js'\n"],"names":["MaskedField","TestEmailButton","Analytics","ConsentProvider","CookieBanner","CookieButton","useConsent","useConsentContext","resolveFormMessage","Turnstile","TurnstileProvider","useTurnstile"],"mappings":"AAAA;AACA,SAASA,WAAW,QAAQ,wDAAuD;AACnF,SAASC,eAAe,QAAQ,4DAA2D;AAC3F,SAASC,SAAS,QAAQ,iCAAgC;AAC1D;;;;;;CAMC,GACD,SACEC,eAAe,EACfC,YAAY,EACZC,YAAY,EACZC,UAAU,EACVC,iBAAiB,QACZ,+BAA8B;AAErC,SAASC,kBAAkB,QAAQ,iDAAgD;AAEnF,SAASC,SAAS,QAAQ,iCAAgC;AAC1D,SAASC,iBAAiB,EAAEC,YAAY,QAAQ,iCAAgC"}
\ No newline at end of file
diff --git a/dist/modules/turnstile/TurnstileProvider.d.ts b/dist/modules/turnstile/TurnstileProvider.d.ts
new file mode 100644
index 0000000..7b1507f
--- /dev/null
+++ b/dist/modules/turnstile/TurnstileProvider.d.ts
@@ -0,0 +1,36 @@
+/**
+ * Provides the Turnstile site key once for the whole app, like ConsentProvider
+ * for cookies. The project reads the key server-side and passes it here in the
+ * layout; forms then use + useTurnstile() with NO per-form
+ * key wiring.
+ *
+ * // layout.tsx (server) → read key, pass to provider
+ * import { TurnstileProvider } from '@intecion/ipal-kit/client'
+ * const siteKey = await getTurnstileSiteKey() // your server helper
+ * {children}
+ *
+ * When siteKey is null (Turnstile not configured), widgets render nothing and
+ * token stays null — forms should treat "no Turnstile" as allowed in dev.
+ */
+export declare function TurnstileProvider({ siteKey, children, }: {
+ siteKey: string | null;
+ children: React.ReactNode;
+}): import("react/jsx-runtime").JSX.Element;
+/**
+ * Hook giving a form the Turnstile token + a widget bound to the provider's key.
+ * No per-form siteKey plumbing — the provider supplies it.
+ *
+ * const { token, TurnstileWidget, reset } = useTurnstile()
+ * // in JSX:
+ * // at submit: submitForm({ ..., turnstileToken: token })
+ * // after submit: reset() // clear for the next submission
+ */
+export declare function useTurnstile(): {
+ token: string | null;
+ TurnstileWidget: (props?: {
+ theme?: 'light' | 'dark' | 'auto';
+ }) => React.ReactNode;
+ reset: () => void;
+ /** True when Turnstile is configured (site key present). */
+ enabled: boolean;
+};
diff --git a/dist/modules/turnstile/TurnstileProvider.js b/dist/modules/turnstile/TurnstileProvider.js
new file mode 100644
index 0000000..a5760b3
--- /dev/null
+++ b/dist/modules/turnstile/TurnstileProvider.js
@@ -0,0 +1,66 @@
+'use client';
+import { jsx as _jsx } from "react/jsx-runtime";
+import { createContext, useCallback, useContext, useState } from 'react';
+import { Turnstile } from './Turnstile.js';
+const TurnstileContext = /*#__PURE__*/ createContext(null);
+/**
+ * Provides the Turnstile site key once for the whole app, like ConsentProvider
+ * for cookies. The project reads the key server-side and passes it here in the
+ * layout; forms then use + useTurnstile() with NO per-form
+ * key wiring.
+ *
+ * // layout.tsx (server) → read key, pass to provider
+ * import { TurnstileProvider } from '@intecion/ipal-kit/client'
+ * const siteKey = await getTurnstileSiteKey() // your server helper
+ * {children}
+ *
+ * When siteKey is null (Turnstile not configured), widgets render nothing and
+ * token stays null — forms should treat "no Turnstile" as allowed in dev.
+ */ export function TurnstileProvider({ siteKey, children }) {
+ const [token, setToken] = useState(null);
+ return /*#__PURE__*/ _jsx(TurnstileContext.Provider, {
+ value: {
+ siteKey,
+ token,
+ setToken
+ },
+ children: children
+ });
+}
+/**
+ * Hook giving a form the Turnstile token + a widget bound to the provider's key.
+ * No per-form siteKey plumbing — the provider supplies it.
+ *
+ * const { token, TurnstileWidget, reset } = useTurnstile()
+ * // in JSX:
+ * // at submit: submitForm({ ..., turnstileToken: token })
+ * // after submit: reset() // clear for the next submission
+ */ export function useTurnstile() {
+ const ctx = useContext(TurnstileContext);
+ if (!ctx) {
+ throw new Error('useTurnstile must be used within ');
+ }
+ const { siteKey, token, setToken } = ctx;
+ const reset = useCallback(()=>setToken(null), [
+ setToken
+ ]);
+ const TurnstileWidget = useCallback((props)=>{
+ if (!siteKey) return null;
+ return /*#__PURE__*/ _jsx(Turnstile, {
+ siteKey: siteKey,
+ onToken: setToken,
+ theme: props?.theme
+ });
+ }, [
+ siteKey,
+ setToken
+ ]);
+ return {
+ token,
+ TurnstileWidget,
+ reset,
+ enabled: Boolean(siteKey)
+ };
+}
+
+//# sourceMappingURL=TurnstileProvider.js.map
\ No newline at end of file
diff --git a/dist/modules/turnstile/TurnstileProvider.js.map b/dist/modules/turnstile/TurnstileProvider.js.map
new file mode 100644
index 0000000..186483b
--- /dev/null
+++ b/dist/modules/turnstile/TurnstileProvider.js.map
@@ -0,0 +1 @@
+{"version":3,"sources":["../../../src/modules/turnstile/TurnstileProvider.tsx"],"sourcesContent":["'use client'\n\nimport { createContext, useCallback, useContext, useState } from 'react'\nimport { Turnstile } from './Turnstile.js'\n\ntype TurnstileContextValue = {\n /** Public site key from the provider (read server-side, passed once). */\n siteKey: string | null\n /** Current token (null until solved / after expiry). */\n token: string | null\n setToken: (t: string | null) => void\n}\n\nconst TurnstileContext = createContext(null)\n\n/**\n * Provides the Turnstile site key once for the whole app, like ConsentProvider\n * for cookies. The project reads the key server-side and passes it here in the\n * layout; forms then use + useTurnstile() with NO per-form\n * key wiring.\n *\n * // layout.tsx (server) → read key, pass to provider\n * import { TurnstileProvider } from '@intecion/ipal-kit/client'\n * const siteKey = await getTurnstileSiteKey() // your server helper\n * {children}\n *\n * When siteKey is null (Turnstile not configured), widgets render nothing and\n * token stays null — forms should treat \"no Turnstile\" as allowed in dev.\n */\nexport function TurnstileProvider({\n siteKey,\n children,\n}: {\n siteKey: string | null\n children: React.ReactNode\n}) {\n const [token, setToken] = useState(null)\n return (\n \n {children}\n \n )\n}\n\n/**\n * Hook giving a form the Turnstile token + a widget bound to the provider's key.\n * No per-form siteKey plumbing — the provider supplies it.\n *\n * const { token, TurnstileWidget, reset } = useTurnstile()\n * // in JSX: \n * // at submit: submitForm({ ..., turnstileToken: token })\n * // after submit: reset() // clear for the next submission\n */\nexport function useTurnstile(): {\n token: string | null\n TurnstileWidget: (props?: { theme?: 'light' | 'dark' | 'auto' }) => React.ReactNode\n reset: () => void\n /** True when Turnstile is configured (site key present). */\n enabled: boolean\n} {\n const ctx = useContext(TurnstileContext)\n if (!ctx) {\n throw new Error('useTurnstile must be used within ')\n }\n const { siteKey, token, setToken } = ctx\n\n const reset = useCallback(() => setToken(null), [setToken])\n\n const TurnstileWidget = useCallback(\n (props?: { theme?: 'light' | 'dark' | 'auto' }) => {\n if (!siteKey) return null\n return \n },\n [siteKey, setToken],\n )\n\n return { token, TurnstileWidget, reset, enabled: Boolean(siteKey) }\n}\n"],"names":["createContext","useCallback","useContext","useState","Turnstile","TurnstileContext","TurnstileProvider","siteKey","children","token","setToken","Provider","value","useTurnstile","ctx","Error","reset","TurnstileWidget","props","onToken","theme","enabled","Boolean"],"mappings":"AAAA;;AAEA,SAASA,aAAa,EAAEC,WAAW,EAAEC,UAAU,EAAEC,QAAQ,QAAQ,QAAO;AACxE,SAASC,SAAS,QAAQ,iBAAgB;AAU1C,MAAMC,iCAAmBL,cAA4C;AAErE;;;;;;;;;;;;;CAaC,GACD,OAAO,SAASM,kBAAkB,EAChCC,OAAO,EACPC,QAAQ,EAIT;IACC,MAAM,CAACC,OAAOC,SAAS,GAAGP,SAAwB;IAClD,qBACE,KAACE,iBAAiBM,QAAQ;QAACC,OAAO;YAAEL;YAASE;YAAOC;QAAS;kBAC1DF;;AAGP;AAEA;;;;;;;;CAQC,GACD,OAAO,SAASK;IAOd,MAAMC,MAAMZ,WAAWG;IACvB,IAAI,CAACS,KAAK;QACR,MAAM,IAAIC,MAAM;IAClB;IACA,MAAM,EAAER,OAAO,EAAEE,KAAK,EAAEC,QAAQ,EAAE,GAAGI;IAErC,MAAME,QAAQf,YAAY,IAAMS,SAAS,OAAO;QAACA;KAAS;IAE1D,MAAMO,kBAAkBhB,YACtB,CAACiB;QACC,IAAI,CAACX,SAAS,OAAO;QACrB,qBAAO,KAACH;YAAUG,SAASA;YAASY,SAAST;YAAUU,OAAOF,OAAOE;;IACvE,GACA;QAACb;QAASG;KAAS;IAGrB,OAAO;QAAED;QAAOQ;QAAiBD;QAAOK,SAASC,QAAQf;IAAS;AACpE"}
\ No newline at end of file
diff --git a/dist/modules/turnstile/client.d.ts b/dist/modules/turnstile/client.d.ts
index 7180b99..44aa3a7 100644
--- a/dist/modules/turnstile/client.d.ts
+++ b/dist/modules/turnstile/client.d.ts
@@ -1,2 +1,3 @@
export { Turnstile } from './Turnstile.js';
export type { TurnstileProps } from './Turnstile.js';
+export { TurnstileProvider, useTurnstile } from './TurnstileProvider.js';
diff --git a/dist/modules/turnstile/client.js b/dist/modules/turnstile/client.js
index 157876e..871f15a 100644
--- a/dist/modules/turnstile/client.js
+++ b/dist/modules/turnstile/client.js
@@ -2,5 +2,6 @@
// Client-only exports — the Turnstile widget. Kept separate from index.ts so
// the server-only verify never leaks into a browser bundle.
export { Turnstile } from './Turnstile.js';
+export { TurnstileProvider, useTurnstile } from './TurnstileProvider.js';
//# sourceMappingURL=client.js.map
\ No newline at end of file
diff --git a/dist/modules/turnstile/client.js.map b/dist/modules/turnstile/client.js.map
index 6beab7c..79a8330 100644
--- a/dist/modules/turnstile/client.js.map
+++ b/dist/modules/turnstile/client.js.map
@@ -1 +1 @@
-{"version":3,"sources":["../../../src/modules/turnstile/client.ts"],"sourcesContent":["'use client'\n// Client-only exports — the Turnstile widget. Kept separate from index.ts so\n// the server-only verify never leaks into a browser bundle.\nexport { Turnstile } from './Turnstile.js'\nexport type { TurnstileProps } from './Turnstile.js'\n"],"names":["Turnstile"],"mappings":"AAAA;AACA,6EAA6E;AAC7E,4DAA4D;AAC5D,SAASA,SAAS,QAAQ,iBAAgB"}
\ No newline at end of file
+{"version":3,"sources":["../../../src/modules/turnstile/client.ts"],"sourcesContent":["'use client'\n// Client-only exports — the Turnstile widget. Kept separate from index.ts so\n// the server-only verify never leaks into a browser bundle.\nexport { Turnstile } from './Turnstile.js'\nexport type { TurnstileProps } from './Turnstile.js'\nexport { TurnstileProvider, useTurnstile } from './TurnstileProvider.js'\n"],"names":["Turnstile","TurnstileProvider","useTurnstile"],"mappings":"AAAA;AACA,6EAA6E;AAC7E,4DAA4D;AAC5D,SAASA,SAAS,QAAQ,iBAAgB;AAE1C,SAASC,iBAAiB,EAAEC,YAAY,QAAQ,yBAAwB"}
\ No newline at end of file
diff --git a/dist/modules/turnstile/index.d.ts b/dist/modules/turnstile/index.d.ts
index fe1cc66..35e57cb 100644
--- a/dist/modules/turnstile/index.d.ts
+++ b/dist/modules/turnstile/index.d.ts
@@ -1 +1,2 @@
+export { TurnstileProvider, useTurnstile } from './TurnstileProvider.js';
export { verifyTurnstile } from './verify.js';
diff --git a/dist/modules/turnstile/index.js b/dist/modules/turnstile/index.js
index 69979b6..90302d3 100644
--- a/dist/modules/turnstile/index.js
+++ b/dist/modules/turnstile/index.js
@@ -1,3 +1,4 @@
+export { TurnstileProvider, useTurnstile } from './TurnstileProvider.js';
// Server-only exports. verify.ts imports 'server-only', so this must never be
// imported from a client component — use ./client for the widget instead.
export { verifyTurnstile } from './verify.js';
diff --git a/dist/modules/turnstile/index.js.map b/dist/modules/turnstile/index.js.map
index acf77d2..48ee066 100644
--- a/dist/modules/turnstile/index.js.map
+++ b/dist/modules/turnstile/index.js.map
@@ -1 +1 @@
-{"version":3,"sources":["../../../src/modules/turnstile/index.ts"],"sourcesContent":["// Server-only exports. verify.ts imports 'server-only', so this must never be\n// imported from a client component — use ./client for the widget instead.\nexport { verifyTurnstile } from './verify.js'\n"],"names":["verifyTurnstile"],"mappings":"AAAA,8EAA8E;AAC9E,0EAA0E;AAC1E,SAASA,eAAe,QAAQ,cAAa"}
\ No newline at end of file
+{"version":3,"sources":["../../../src/modules/turnstile/index.ts"],"sourcesContent":["export { TurnstileProvider, useTurnstile } from './TurnstileProvider.js'\n// Server-only exports. verify.ts imports 'server-only', so this must never be\n// imported from a client component — use ./client for the widget instead.\nexport { verifyTurnstile } from './verify.js'\n"],"names":["TurnstileProvider","useTurnstile","verifyTurnstile"],"mappings":"AAAA,SAASA,iBAAiB,EAAEC,YAAY,QAAQ,yBAAwB;AACxE,8EAA8E;AAC9E,0EAA0E;AAC1E,SAASC,eAAe,QAAQ,cAAa"}
\ No newline at end of file
diff --git a/docs/turnstile.md b/docs/turnstile.md
index 7f3593e..9c62993 100644
--- a/docs/turnstile.md
+++ b/docs/turnstile.md
@@ -58,4 +58,73 @@ Zwraca `false` na każdy problem (brak klucza, sieć, odrzucenie) — traktuj
trafi do bundla przeglądarki.
> W formularzach zwykle nie wołasz `verifyTurnstile` wprost — robi to
-> `submitForm` (patrz [forms.md](./forms.md)).
\ No newline at end of file
+> `submitForm` (patrz [forms.md](./forms.md)).
+
+## Uproszczone wpięcie — TurnstileProvider + useTurnstile (zalecane)
+
+Jak CookieBanner: siteKey raz w layoutcie, formularze biorą z kontekstu. Koniec
+przekazywania siteKey do każdego formularza.
+
+### 1. Provider w layoutcie (raz, siteKey z serwera)
+
+```tsx
+// app/(frontend)/[locale]/layout.tsx (server)
+import { TurnstileProvider } from '@intecion/ipal-kit/client'
+import { getTurnstileSiteKey } from '@/lib/payload' // Twój helper server-side
+
+export default async function Layout({ children }) {
+ const siteKey = await getTurnstileSiteKey() // z panelu (SiteIntegrations)
+ return (
+
+
+
+ {children}
+
+
+
+ )
+}
+```
+
+### 2. Formularz — useTurnstile (zero plumbingu siteKey)
+
+```tsx
+'use client'
+import { useTurnstile } from '@intecion/ipal-kit/client'
+
+function ContactForm() {
+ const { token, TurnstileWidget, reset, enabled } = useTurnstile()
+
+ async function handleSubmit(data) {
+ const result = await submitForm({ ...data, turnstileToken: token })
+ if (result.ok) reset() // wyczyść token na następne wysłanie
+ }
+
+ return (
+
+ )
+}
+```
+
+`token` → do submitForm. `TurnstileWidget` → wstaw gdzie ma być. `reset()` → po
+wysłaniu. `enabled` → false gdy brak klucza (dev bez Turnstile).
+
+### Dlaczego Turnstile NIE jest w pełni "wstaw i zapomnij" jak CookieBanner
+
+CookieBanner jest samodzielny (renderuje się, zarządza zgodą, zero interakcji).
+Turnstile z natury jest CZĘŚCIĄ formularza — zwraca token, który formularz musi
+wysłać przy submit i zweryfikować server-side. Nie da się go „wstawić
+gdziekolwiek" — musi być w formularzu, przy jego logice wysyłki.
+
+Provider+hook to maksimum uproszczenia: siteKey raz (jak CookieBanner), a w
+formularzu tylko `` + `token`. Reszta (weryfikacja) dzieje się
+w submitForm automatycznie.
+
+### Stary sposób (nadal działa)
+
+`` bezpośrednio — jeśli potrzebujesz
+pełnej kontroli albo masz nietypowy przypadek. Provider to warstwa wygody nad tym.
\ No newline at end of file
diff --git a/src/exports/client.ts b/src/exports/client.ts
index 4b1ab86..3780132 100644
--- a/src/exports/client.ts
+++ b/src/exports/client.ts
@@ -17,8 +17,8 @@ export {
useConsentContext,
} from '../modules/consent/client.js'
export type { CookieBannerClassNames } from '../modules/consent/client.js'
-export { Turnstile } from '../modules/turnstile/client.js'
-export type { TurnstileProps } from '../modules/turnstile/client.js'
-
export { resolveFormMessage } from '../modules/notifications/resolveFormMessage.js'
export type { FormNotificationTexts } from '../modules/notifications/types.js'
+export { Turnstile } from '../modules/turnstile/client.js'
+export { TurnstileProvider, useTurnstile } from '../modules/turnstile/client.js'
+export type { TurnstileProps } from '../modules/turnstile/client.js'
diff --git a/src/modules/turnstile/TurnstileProvider.tsx b/src/modules/turnstile/TurnstileProvider.tsx
new file mode 100644
index 0000000..077662e
--- /dev/null
+++ b/src/modules/turnstile/TurnstileProvider.tsx
@@ -0,0 +1,78 @@
+'use client'
+
+import { createContext, useCallback, useContext, useState } from 'react'
+import { Turnstile } from './Turnstile.js'
+
+type TurnstileContextValue = {
+ /** Public site key from the provider (read server-side, passed once). */
+ siteKey: string | null
+ /** Current token (null until solved / after expiry). */
+ token: string | null
+ setToken: (t: string | null) => void
+}
+
+const TurnstileContext = createContext(null)
+
+/**
+ * Provides the Turnstile site key once for the whole app, like ConsentProvider
+ * for cookies. The project reads the key server-side and passes it here in the
+ * layout; forms then use + useTurnstile() with NO per-form
+ * key wiring.
+ *
+ * // layout.tsx (server) → read key, pass to provider
+ * import { TurnstileProvider } from '@intecion/ipal-kit/client'
+ * const siteKey = await getTurnstileSiteKey() // your server helper
+ * {children}
+ *
+ * When siteKey is null (Turnstile not configured), widgets render nothing and
+ * token stays null — forms should treat "no Turnstile" as allowed in dev.
+ */
+export function TurnstileProvider({
+ siteKey,
+ children,
+}: {
+ siteKey: string | null
+ children: React.ReactNode
+}) {
+ const [token, setToken] = useState(null)
+ return (
+
+ {children}
+
+ )
+}
+
+/**
+ * Hook giving a form the Turnstile token + a widget bound to the provider's key.
+ * No per-form siteKey plumbing — the provider supplies it.
+ *
+ * const { token, TurnstileWidget, reset } = useTurnstile()
+ * // in JSX:
+ * // at submit: submitForm({ ..., turnstileToken: token })
+ * // after submit: reset() // clear for the next submission
+ */
+export function useTurnstile(): {
+ token: string | null
+ TurnstileWidget: (props?: { theme?: 'light' | 'dark' | 'auto' }) => React.ReactNode
+ reset: () => void
+ /** True when Turnstile is configured (site key present). */
+ enabled: boolean
+} {
+ const ctx = useContext(TurnstileContext)
+ if (!ctx) {
+ throw new Error('useTurnstile must be used within ')
+ }
+ const { siteKey, token, setToken } = ctx
+
+ const reset = useCallback(() => setToken(null), [setToken])
+
+ const TurnstileWidget = useCallback(
+ (props?: { theme?: 'light' | 'dark' | 'auto' }) => {
+ if (!siteKey) return null
+ return
+ },
+ [siteKey, setToken],
+ )
+
+ return { token, TurnstileWidget, reset, enabled: Boolean(siteKey) }
+}
diff --git a/src/modules/turnstile/client.ts b/src/modules/turnstile/client.ts
index b586876..2c52976 100644
--- a/src/modules/turnstile/client.ts
+++ b/src/modules/turnstile/client.ts
@@ -3,3 +3,4 @@
// the server-only verify never leaks into a browser bundle.
export { Turnstile } from './Turnstile.js'
export type { TurnstileProps } from './Turnstile.js'
+export { TurnstileProvider, useTurnstile } from './TurnstileProvider.js'
diff --git a/src/modules/turnstile/index.ts b/src/modules/turnstile/index.ts
index 14ae88e..98b88e4 100644
--- a/src/modules/turnstile/index.ts
+++ b/src/modules/turnstile/index.ts
@@ -1,3 +1,4 @@
+export { TurnstileProvider, useTurnstile } from './TurnstileProvider.js'
// Server-only exports. verify.ts imports 'server-only', so this must never be
// imported from a client component — use ./client for the widget instead.
export { verifyTurnstile } from './verify.js'