{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "knock-codes-template",
  "title": "Knock Codes Template",
  "description": "A complete, single-file \"restricted access\" screen — full-page dark backdrop, centered card, segmented code entry, support link, footer help text. Everything in one component.",
  "registryDependencies": [
    "https://knock.codes/r/react/knock-codes-hook.json",
    "https://knock.codes/r/react/cx-util.json",
    "https://knock.codes/r/react/pin-input.json"
  ],
  "files": [
    {
      "path": "packages/react/KnockCodesTemplate.tsx",
      "content": "// Knock Codes Template v1.0.0\n\"use client\";\n\nimport { useEffect, useState, type ReactNode } from \"react\";\nimport { useKnockCodes } from \"./useKnockCodes.ts\";\nimport { PinInput } from \"./PinInput.tsx\";\nimport { DEFAULT_LABELS, type KnockCodesConfig, type KnockCodesLabels } from \"./types.ts\";\nimport { cx } from \"./cx.ts\";\n\n// Shake on a failed attempt, fade+slide on unlock — inlined via a plain\n// `<style>` tag (not a Tailwind config keyframe) so this file works\n// standalone in a host project that has no matching keyframe of its own.\n// Both keyframes are wrapped in the reduced-motion media query rather than\n// toggled from JS: under `prefers-reduced-motion: reduce`, the referenced\n// keyframe names simply don't exist, so `animation: knock-codes-shake …`\n// resolves to no visual effect — no separate JS branch to keep in sync.\nconst KNOCK_CODES_MOTION_KEYFRAMES = `@media (prefers-reduced-motion: no-preference) {\n  @keyframes knock-codes-shake {\n    10%, 90% { transform: translateX(-1px); }\n    20%, 80% { transform: translateX(2px); }\n    30%, 50%, 70% { transform: translateX(-4px); }\n    40%, 60% { transform: translateX(4px); }\n  }\n  @keyframes knock-codes-reveal {\n    from { opacity: 0; transform: translateY(4px); }\n    to { opacity: 1; transform: translateY(0); }\n  }\n}`;\n\nexport interface KnockCodesTemplateLabels extends KnockCodesLabels {\n  description?: string;\n  accessCodeLabel?: string;\n  supportLabel?: string;\n  footerText?: ReactNode;\n}\n\nexport interface KnockCodesTemplateProps extends KnockCodesConfig {\n  children: ReactNode;\n  /** Rendered above the heading — your own logo/wordmark. Omitted entirely if not passed. */\n  logo?: ReactNode;\n  labels?: KnockCodesTemplateLabels;\n  /** Total code length, split into groups of `groupSize` with a dash between. @default 8 */\n  codeLength?: number;\n  /** @default 4 */\n  groupSize?: number;\n  /** Renders \"Contact Support\" as a link. Ignored if `onContactSupport` is also set. */\n  supportHref?: string;\n  /** Renders \"Contact Support\" as a button instead of a link. */\n  onContactSupport?: () => void;\n  /** Set false to embed this somewhere other than a real page root (a demo, a docs preview) — drops the full-viewport (100dvh) sizing. @default true */\n  fullPage?: boolean;\n  /**\n   * Forces light or dark presentation on its own, independent of any\n   * ancestor \".dark\" class — this is a single, standalone file, and\n   * shouldn't need the host app to already have dark-mode wiring (e.g.\n   * next-themes) in place just to show its dark variant. Omit to follow\n   * the nearest \".dark\" ancestor if one happens to exist.\n   */\n  theme?: \"light\" | \"dark\";\n  /**\n   * Persist the unlocked session across reloads within the same tab via\n   * sessionStorage. Not a security boundary — clearable from DevTools or a\n   * private window, same as any other client-side storage. @default undefined (off)\n   */\n  remember?: \"session\";\n  autoFocus?: boolean;\n  className?: string;\n}\n\nconst TEMPLATE_LABELS: Required<KnockCodesTemplateLabels> = {\n  ...DEFAULT_LABELS,\n  heading: \"Restricted Access\",\n  submitLabel: \"Unlock Access\",\n  description: \"This feature is currently protected. Enter your access code to continue.\",\n  accessCodeLabel: \"Access code\",\n  supportLabel: \"Contact Support\",\n  footerText: \"Don't have an access code? Contact your administrator or support team for assistance.\",\n};\n\nfunction SuccessView({ theme }: { theme?: \"light\" | \"dark\" }) {\n  const successPanel = (\n    <div className=\"flex h-full min-h-[26rem] w-full items-center justify-center bg-[var(--ag-canvas-bg,#e5e7eb)] p-6 dark:bg-[var(--ag-canvas-bg-dark,#0b1220)]\">\n      <style>{KNOCK_CODES_MOTION_KEYFRAMES}</style>\n      <div className=\"flex flex-col items-center gap-2 text-center animate-[knock-codes-reveal_0.3s_ease-out]\">\n        <div className=\"flex h-10 w-10 items-center justify-center rounded-full bg-green-100 text-green-600 dark:bg-green-500/10 dark:text-green-400\">\n          <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth={2} className=\"h-5 w-5\">\n            <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M5 13l4 4L19 7\" />\n          </svg>\n        </div>\n        <p className=\"text-sm font-medium text-gray-700 dark:text-gray-200\">Access granted</p>\n      </div>\n    </div>\n  );\n  return theme === \"dark\" ? <div className=\"dark h-full w-full\">{successPanel}</div> : successPanel;\n}\n\n/**\n * A complete, single-drop-in \"restricted access\" screen — full-page dark\n * backdrop, centered card, segmented code entry, support link, and footer\n * help text, all in one file. Built on the same `useKnockCodes`\n * session/verification contract as every other block, just with a\n * different presentation. Segmented entry is `<PinInput variant=\"boxes\">`.\n */\nexport function KnockCodesTemplate({\n  children,\n  logo,\n  labels,\n  codeLength = 8,\n  groupSize = 4,\n  supportHref,\n  onContactSupport,\n  fullPage = true,\n  theme,\n  remember,\n  autoFocus = true,\n  className,\n  ...config\n}: KnockCodesTemplateProps) {\n  const merged = { ...TEMPLATE_LABELS, ...labels };\n  const { state, error, submit, ready } = useKnockCodes({\n    ...config,\n    storage: remember === \"session\" ? \"sessionStorage\" : config.storage,\n  });\n  const [code, setCode] = useState(\"\");\n  const [showChildren, setShowChildren] = useState(false);\n\n  // Holds the unlock screen visible for a beat so success has a visible\n  // transition instead of an instant swap to `children`.\n  useEffect(() => {\n    if (state !== \"unlocked\") {\n      setShowChildren(false);\n      return;\n    }\n    const timer = setTimeout(() => setShowChildren(true), 550);\n    return () => clearTimeout(timer);\n  }, [state]);\n\n  if (!ready) return null;\n\n  if (state === \"unlocked\") {\n    if (!showChildren) {\n      return <SuccessView theme={theme} />;\n    }\n    return (\n      <div className=\"animate-[knock-codes-reveal_0.35s_ease-out]\">\n        <style>{KNOCK_CODES_MOTION_KEYFRAMES}</style>\n        {children}\n      </div>\n    );\n  }\n\n  const handleSubmit = async () => {\n    if (!code || state === \"submitting\") return;\n    await submit(code);\n    setCode(\"\");\n  };\n\n  // Tailwind's `dark:` utilities only activate for descendants of a \".dark\"\n  // ancestor — never for the element carrying that class itself — so\n  // forcing a theme needs this extra wrapper, not just a class on the root\n  // below. Omitting `theme` renders exactly as before (whatever ambient\n  // \".dark\" ancestor happens to exist, or none).\n  const content = (\n    <div\n      className={cx(\n        \"flex w-full items-center justify-center bg-[var(--ag-canvas-bg,#e5e7eb)] p-6 dark:bg-[var(--ag-canvas-bg-dark,#0b1220)]\",\n        // Real page-root usage wants the full viewport; embedded usage\n        // (a demo, a docs preview) wants to fill whatever height its own\n        // container was given instead — the container providing a real,\n        // definite height is that container's job, not this component's.\n        fullPage ? \"min-h-[100dvh]\" : \"h-full\",\n        className\n      )}\n    >\n      <div\n        style={{ fontFamily: \"var(--ag-font, inherit)\" }}\n        className=\"w-full max-w-[27rem] rounded-[var(--ag-radius,1rem)] bg-[var(--ag-card,#ffffff)] p-8 shadow-2xl dark:bg-[var(--ag-card-dark,#030712)]\"\n      >\n        <style>{KNOCK_CODES_MOTION_KEYFRAMES}</style>\n        {logo && <div className=\"mb-6\">{logo}</div>}\n\n        <h1 className=\"text-2xl font-bold text-gray-900 dark:text-gray-50\">{merged.heading}</h1>\n        <p className=\"mt-2 text-sm text-gray-500 dark:text-gray-400\">{merged.description}</p>\n\n        <div className=\"mt-6\">\n          <PinInput\n            variant=\"boxes\"\n            length={codeLength}\n            groupSize={groupSize}\n            value={code}\n            onChange={setCode}\n            onSubmit={() => void handleSubmit()}\n            submitting={state === \"submitting\"}\n            error={error}\n            labels={{ ...labels, inputLabel: merged.accessCodeLabel, submitLabel: merged.submitLabel }}\n            autoFocus={autoFocus}\n          />\n        </div>\n\n        {(supportHref || onContactSupport) && (\n          <div className=\"mt-3 text-center\">\n            {onContactSupport ? (\n              <button\n                type=\"button\"\n                onClick={onContactSupport}\n                className=\"text-sm font-medium text-red-600 hover:underline dark:text-red-400\"\n              >\n                {merged.supportLabel}\n              </button>\n            ) : (\n              <a href={supportHref} className=\"text-sm font-medium text-red-600 hover:underline dark:text-red-400\">\n                {merged.supportLabel}\n              </a>\n            )}\n          </div>\n        )}\n\n        <hr className=\"my-5 border-gray-200 dark:border-gray-800\" />\n\n        <p className=\"text-center text-xs text-gray-400 dark:text-gray-500\">{merged.footerText}</p>\n      </div>\n    </div>\n  );\n\n  // A plain sized wrapper, not `display: contents` — `contents` has\n  // cross-browser quirks around passing percentage sizing through to\n  // children, which broke the fill-the-canvas behavior above.\n  return theme === \"dark\" ? <div className=\"dark h-full w-full\">{content}</div> : content;\n}\n",
      "type": "registry:file",
      "target": "components/knock-codes/react/KnockCodesTemplate.tsx"
    }
  ],
  "type": "registry:block"
}