{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "minimal-access-template",
  "title": "Minimal Access Template",
  "description": "The leanest access screen — a single masked field, a small plain card, no segmented boxes and no default footer copy. 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/MinimalAccessTemplate.tsx",
      "content": "// Minimal 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 card on a failed attempt — inlined via a plain `<style>` tag\n// (not a Tailwind config keyframe) so this file works standalone in a host\n// 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: minimal-access-shake …` utility below resolves to no\n// visual effect.\nconst MINIMAL_ACCESS_SHAKE_KEYFRAMES = `@media (prefers-reduced-motion: no-preference) {\n  @keyframes minimal-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 MinimalAccessTemplateLabels extends KnockCodesLabels {\n  description?: string;\n  supportLabel?: string;\n  footerText?: ReactNode;\n}\n\nexport interface MinimalAccessTemplateProps extends KnockCodesConfig {\n  children: ReactNode;\n  /** Rendered above the heading — your own logo/wordmark. Omitted entirely if not passed. */\n  logo?: ReactNode;\n  labels?: MinimalAccessTemplateLabels;\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<MinimalAccessTemplateLabels> = {\n  ...DEFAULT_LABELS,\n  heading: \"Enter access code\",\n  description: \"This page is private.\",\n  supportLabel: \"Contact support\",\n  footerText: \"\",\n};\n\n/**\n * The leanest possible access screen — a single masked field, a\n * small plain card, no segmented boxes and no default footer copy. Same\n * `useKnockCodes` contract as every other block, just the smallest possible\n * presentation. For segmented code entry, use `<KnockCodesTemplate>` instead.\n */\nexport function MinimalAccessTemplate({\n  children,\n  logo,\n  labels,\n  supportHref,\n  onContactSupport,\n  fullPage = true,\n  theme,\n  remember,\n  autoFocus = true,\n  className,\n  ...config\n}: MinimalAccessTemplateProps) {\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\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-canvas-bg,#f9fafb)] p-6 dark:bg-[var(--ag-canvas-bg-dark,#0b1220)]\">\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  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\n      className={cx(\n        \"flex w-full items-center justify-center bg-[var(--ag-canvas-bg,#f9fafb)] p-6 dark:bg-[var(--ag-canvas-bg-dark,#0b1220)]\",\n        fullPage ? \"min-h-[100dvh]\" : \"h-full\",\n        className\n      )}\n    >\n      <div\n        key={shakeSeed}\n        style={{ fontFamily: \"var(--ag-font, inherit)\" }}\n        className={cx(\n          \"w-full max-w-sm rounded-[var(--ag-radius,0.75rem)] border border-[var(--ag-border,#e5e7eb)] bg-[var(--ag-card,#ffffff)] p-7 dark:border-[var(--ag-border-dark,#1f2937)] dark:bg-[var(--ag-card-dark,#030712)]\",\n          shakeSeed > 0 && \"animate-[minimal-access-shake_0.4s_ease-in-out]\"\n        )}\n      >\n        <style>{MINIMAL_ACCESS_SHAKE_KEYFRAMES}</style>\n        {logo && <div className=\"mb-5\">{logo}</div>}\n\n        <h1 className=\"text-lg font-semibold text-gray-900 dark:text-gray-50\">{merged.heading}</h1>\n        {merged.description && <p className=\"mt-1 text-sm text-gray-500 dark:text-gray-400\">{merged.description}</p>}\n\n        <div className=\"mt-5\">\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-xs font-medium text-gray-500 hover:underline dark:text-gray-400\">\n                {merged.supportLabel}\n              </button>\n            ) : (\n              <a href={supportHref} className=\"text-xs font-medium text-gray-500 hover:underline dark:text-gray-400\">\n                {merged.supportLabel}\n              </a>\n            )}\n          </div>\n        )}\n\n        {merged.footerText && <p className=\"mt-4 text-center text-xs text-gray-400 dark:text-gray-500\">{merged.footerText}</p>}\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/MinimalAccessTemplate.tsx"
    }
  ],
  "type": "registry:block"
}