{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "branded-access-template",
  "title": "Branded Access Template",
  "description": "A split-screen, logo-forward access screen — brand panel on one side, code entry on the other. The brand panel collapses on small screens.",
  "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/BrandedAccessTemplate.tsx",
      "content": "// Branded Access 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// Shakes the form column on a failed attempt — 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// Wrapped in the reduced-motion query rather than toggled from JS: under\n// `prefers-reduced-motion: reduce` this keyframe name simply doesn't exist,\n// so the `animation: branded-access-shake …` utility below resolves to no\n// visual effect.\nconst BRANDED_ACCESS_SHAKE_KEYFRAMES = `@media (prefers-reduced-motion: no-preference) {\n  @keyframes branded-access-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}`;\n\nexport interface BrandedAccessTemplateLabels extends KnockCodesLabels {\n  description?: string;\n  supportLabel?: string;\n  footerText?: ReactNode;\n}\n\nexport interface BrandedAccessTemplateProps extends KnockCodesConfig {\n  children: ReactNode;\n  /** Rendered on the brand panel, above the tagline — your own logo/wordmark. */\n  logo?: ReactNode;\n  /** Short line under the logo on the brand panel. */\n  tagline?: ReactNode;\n  labels?: BrandedAccessTemplateLabels;\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. @default true */\n  fullPage?: boolean;\n  /** Forces light or dark presentation, independent of any ancestor \".dark\" class. Omit to follow one if it exists. */\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<BrandedAccessTemplateLabels> = {\n  ...DEFAULT_LABELS,\n  heading: \"Welcome back\",\n  description: \"Enter your access code to continue to your workspace.\",\n  submitLabel: \"Continue\",\n  supportLabel: \"Contact support\",\n  footerText: \"Don't have a code? Contact your administrator or support team for assistance.\",\n};\n\nfunction SuccessPanel({ theme }: { theme?: \"light\" | \"dark\" }) {\n  const panel = (\n    <div className=\"flex h-full min-h-[26rem] w-full items-center justify-center bg-[var(--ag-card,#ffffff)] p-6 dark:bg-[var(--ag-card-dark,#030712)]\">\n      <div className=\"flex flex-col items-center gap-2 text-center\">\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\">{panel}</div> : panel;\n}\n\n/**\n * A split-screen, logo-forward access screen — brand panel on\n * one side, code entry on the other. The brand panel collapses on small\n * screens so the form stays the only thing visible on mobile. Same\n * `useKnockCodes` contract as every other block.\n */\nexport function BrandedAccessTemplate({\n  children,\n  logo,\n  tagline,\n  labels,\n  supportHref,\n  onContactSupport,\n  fullPage = true,\n  theme,\n  remember,\n  autoFocus = true,\n  className,\n  ...config\n}: BrandedAccessTemplateProps) {\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 [shakeSeed, setShakeSeed] = useState(0);\n  const [showChildren, setShowChildren] = useState(false);\n\n  useEffect(() => {\n    if (error) setShakeSeed((seed) => seed + 1);\n  }, [error]);\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 <SuccessPanel theme={theme} />;\n    }\n    return <>{children}</>;\n  }\n\n  const handleSubmit = async () => {\n    if (!code || state === \"submitting\") return;\n    await submit(code);\n    setCode(\"\");\n  };\n\n  const content = (\n    <div className={cx(\"grid w-full lg:grid-cols-2\", fullPage ? \"min-h-[100dvh]\" : \"h-full\", className)}>\n      <div className=\"hidden flex-col justify-center bg-gradient-to-br from-[var(--ag-primary,#2563eb)] to-[var(--ag-accent,#3730a3)] p-12 text-white lg:flex\">\n        {logo && <div className=\"mb-6\">{logo}</div>}\n        {tagline && <p className=\"max-w-sm text-lg text-white/80\">{tagline}</p>}\n      </div>\n\n      <div className=\"flex items-center justify-center bg-[var(--ag-card,#ffffff)] p-6 dark:bg-[var(--ag-card-dark,#030712)] lg:p-12\">\n        <div\n          key={shakeSeed}\n          style={{ fontFamily: \"var(--ag-font, inherit)\" }}\n          className={cx(\"w-full max-w-sm\", shakeSeed > 0 && \"animate-[branded-access-shake_0.4s_ease-in-out]\")}\n        >\n          <style>{BRANDED_ACCESS_SHAKE_KEYFRAMES}</style>\n          {logo && <div className=\"mb-6 lg:hidden\">{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              value={code}\n              onChange={setCode}\n              onSubmit={() => void handleSubmit()}\n              submitting={state === \"submitting\"}\n              error={error}\n              labels={labels}\n              autoFocus={autoFocus}\n            />\n          </div>\n\n          {(supportHref || onContactSupport) && (\n            <div className=\"mt-3 text-center\">\n              {onContactSupport ? (\n                <button type=\"button\" onClick={onContactSupport} className=\"text-sm font-medium text-[var(--ag-primary,#2563eb)] hover:underline dark:text-[var(--ag-primary-dark,#60a5fa)]\">\n                  {merged.supportLabel}\n                </button>\n              ) : (\n                <a href={supportHref} className=\"text-sm font-medium text-[var(--ag-primary,#2563eb)] hover:underline dark:text-[var(--ag-primary-dark,#60a5fa)]\">\n                  {merged.supportLabel}\n                </a>\n              )}\n            </div>\n          )}\n\n          <p className=\"mt-6 text-center text-xs text-gray-400 dark:text-gray-500\">{merged.footerText}</p>\n        </div>\n      </div>\n    </div>\n  );\n\n  return theme === \"dark\" ? <div className=\"dark h-full w-full\">{content}</div> : content;\n}\n",
      "type": "registry:file",
      "target": "components/knock-codes/react/BrandedAccessTemplate.tsx"
    }
  ],
  "type": "registry:block"
}