{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "knock-codes-hook",
  "title": "useKnockCodes",
  "description": "The headless session/verification hook every stateful block is built on. Installed automatically — use it directly only if you're building a fully custom PIN UI.",
  "registryDependencies": [
    "https://knock.codes/r/react/knock-codes-types.json"
  ],
  "files": [
    {
      "path": "packages/react/useKnockCodes.ts",
      "content": "\"use client\";\r\n\r\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\r\nimport { resolveVerifyFn, type VerifyResult } from \"../core/verify.ts\";\r\nimport { createSession, isExpired, touchExpiry, type KnockCodesSession } from \"../core/session.ts\";\r\nimport { createSessionStore } from \"../core/storage.ts\";\r\nimport {\r\n  ACTIVITY_WRITE_THROTTLE_MS,\r\n  DEFAULT_TIMEOUT_MS,\r\n  EXPIRY_POLL_INTERVAL_MS,\r\n  type KnockCodesConfig,\r\n  type KnockCodesError,\r\n  type KnockCodesState,\r\n  type UseKnockCodesResult,\r\n} from \"./types.ts\";\r\n\r\nconst ACTIVITY_EVENTS = [\"pointerdown\", \"keydown\", \"scroll\"] as const;\r\n\r\n/**\r\n * Headless session/verification hook. `<KnockCodes>` is a thin renderer\r\n * over this; a host app can call it directly to build a fully custom PIN\r\n * UI while the session contract stays identical either way.\r\n */\r\nexport function useKnockCodes(config: KnockCodesConfig): UseKnockCodesResult {\r\n  const {\r\n    expectedHash,\r\n    verify,\r\n    storage = \"localStorage\",\r\n    storageKey,\r\n    timeout = DEFAULT_TIMEOUT_MS,\r\n    activityTracking = false,\r\n  } = config;\r\n\r\n  // Resolved once per config identity; throws synchronously (during render)\r\n  // on misconfiguration — no dev/prod branch here, matching verify.ts's\r\n  // own \"no dev/prod branch\" stance.\r\n  const verifyFn = useMemo(() => resolveVerifyFn({ expectedHash, verify }), [expectedHash, verify]);\r\n  const store = useMemo(() => createSessionStore(storage, { storageKey }), [storage, storageKey]);\r\n\r\n  const [session, setSession] = useState<KnockCodesSession | null>(null);\r\n  const [submitting, setSubmitting] = useState(false);\r\n  const [error, setError] = useState<KnockCodesError | null>(null);\r\n\r\n  // Initial read is deferred to an effect (never runs during SSR) so the\r\n  // server-rendered/first-paint markup never depends on storage that may not\r\n  // exist yet — storage.ts throws rather than silently no-op-ing.\r\n  useEffect(() => {\r\n    const current = store.get();\r\n    setSession(current && !isExpired(current) ? current : null);\r\n  }, [store]);\r\n\r\n  // Expiry: checked on an interval and on tab focus.\r\n  useEffect(() => {\r\n    const checkExpiry = () => {\r\n      setSession((current) => {\r\n        if (!current || !isExpired(current)) return current;\r\n        store.clear();\r\n        return null;\r\n      });\r\n    };\r\n    const interval = setInterval(checkExpiry, EXPIRY_POLL_INTERVAL_MS);\r\n    window.addEventListener(\"focus\", checkExpiry);\r\n    return () => {\r\n      clearInterval(interval);\r\n      window.removeEventListener(\"focus\", checkExpiry);\r\n    };\r\n  }, [store]);\r\n\r\n  // Cross-tab sync: only the localStorage-backed store ever actually calls\r\n  // back here — sessionStorage/memory stores' subscribe is a no-op.\r\n  useEffect(() => store.subscribe(() => {\r\n    const current = store.get();\r\n    setSession(current && !isExpired(current) ? current : null);\r\n  }), [store]);\r\n\r\n  // Activity tracking (opt-in): sliding-timeout model, throttled writes.\r\n  const lastTouchAtRef = useRef(0);\r\n  useEffect(() => {\r\n    if (!activityTracking) return;\r\n    const onActivity = () => {\r\n      const now = Date.now();\r\n      if (now - lastTouchAtRef.current < ACTIVITY_WRITE_THROTTLE_MS) return;\r\n      lastTouchAtRef.current = now;\r\n      setSession((current) => {\r\n        if (!current || isExpired(current, now)) return current;\r\n        const touched = touchExpiry(current, timeout, now);\r\n        store.set(touched);\r\n        return touched;\r\n      });\r\n    };\r\n    for (const type of ACTIVITY_EVENTS) window.addEventListener(type, onActivity);\r\n    return () => {\r\n      for (const type of ACTIVITY_EVENTS) window.removeEventListener(type, onActivity);\r\n    };\r\n  }, [activityTracking, timeout, store]);\r\n\r\n  // A ref, not the `submitting` state, guards re-entrancy: two synchronous\r\n  // back-to-back calls to the same submit closure (e.g. a double-click before\r\n  // React re-renders) would otherwise both read the same stale `submitting`\r\n  // value and both proceed.\r\n  const submittingRef = useRef(false);\r\n\r\n  const submit = useCallback(\r\n    async (code: string) => {\r\n      if (code.length === 0) return; // no network/hash call for an empty submit\r\n      if (submittingRef.current) return; // a submit already in flight — ignore re-entrant calls rather than racing two verifications\r\n      submittingRef.current = true;\r\n      setSubmitting(true);\r\n      setError(null);\r\n\r\n      let result: VerifyResult;\r\n      try {\r\n        result = await verifyFn(code);\r\n      } catch {\r\n        // A throwing VerifyFn is treated as a network failure.\r\n        result = { ok: false, reason: \"network\" };\r\n      }\r\n\r\n      submittingRef.current = false;\r\n      setSubmitting(false);\r\n      if (result.ok) {\r\n        const next = createSession(result, timeout);\r\n        store.set(next);\r\n        setSession(next);\r\n      } else {\r\n        // \"unknown\"/omitted collapses into \"invalid\".\r\n        setError({ reason: result.reason === \"network\" ? \"network\" : \"invalid\" });\r\n      }\r\n    },\r\n    [verifyFn, store, timeout]\r\n  );\r\n\r\n  const logout = useCallback(() => {\r\n    store.clear();\r\n    setSession(null);\r\n  }, [store]);\r\n\r\n  const state: KnockCodesState = submitting ? \"submitting\" : session ? \"unlocked\" : \"idle\";\r\n\r\n  return {\r\n    state,\r\n    error: state === \"idle\" ? error : null, // error is an annotation on idle, not its own state\r\n    session,\r\n    submit,\r\n    logout,\r\n  };\r\n}\r\n",
      "type": "registry:file",
      "target": "components/knock-codes/react/useKnockCodes.ts"
    }
  ],
  "type": "registry:block"
}