{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "cooldown-notice",
  "type": "registry:component",
  "title": "Cooldown notice",
  "description": "Displays a calm countdown without enforcing a retry policy. Does not verify codes, persist state, or grant or revoke access. Contract: the consumer owns lifecycle timestamps and action callbacks.",
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/components/cooldown-notice.tsx",
      "type": "registry:component",
      "target": "@components/knock/cooldown-notice.tsx",
      "content": "/**\n * MIT License — Copyright (c) 2026 Knock contributors.\n * Displays a calm countdown without enforcing a retry policy.\n * Does not verify codes, persist state, or grant or revoke access.\n * Contract: the consumer owns lifecycle timestamps and action callbacks.\n * @version 0.2.0\n * @minimalExample <CooldownNotice cooldownUntil=\"2030-01-01T12:00:00Z\" />\n * @fullExample <CooldownNotice cooldownUntil=\"2030-01-01T12:00:00Z\" theme=\"light\" labels={{ heading: \"Take a moment.\", description: \"Your preview will be here.\", waiting: \"Please wait a little.\", ready: \"You can try again.\", unavailable: \"Ask the preview owner.\", countdown: seconds => `${seconds}s` }} />\n * @lifecycle gate\n * @whenToUse A retry window is running and the gate should stay on screen, with the wait stated calmly beside the code field.\n * @notFor A full screen that replaces the gate during the pause. Use the Cooldown screen block.\n * @pitfall Reaching zero announces readiness and nothing else. It never submits, never re-enables your submit button and never clears the cooldown.\n * @pitfall `cooldownUntil` is a timestamp you own. A value already in the past renders the ready state immediately; the component counts no attempts.\n * @a11y Native controls, visible focus, localized labels; lifecycle messages use polite announcements. No modal dialog is created.\n */\n'use client';\n\nimport { useEffect, useState } from 'react';\nexport interface CooldownNoticeProps {\n  /** Retry deadline as ISO 8601 with timezone; required. Invalid values show unavailable text. */\n  cooldownUntil: string;\n  /** Localized visible text and countdown formatter. */\n  labels?: {\n    /** Localized heading. */\n    heading?: string;\n\n    /** Localized description. */\n    description?: string;\n\n    /** Localized ready. */\n    ready?: string;\n\n    /** Localized waiting. */\n    waiting?: string;\n\n    /** Localized unavailable. */\n    unavailable?: string;\n\n    /** Localized countdown formatter. */\n    countdown?: (seconds: number) => string;\n  };\n\n  /** Explicit theme; omitted follows the system preference. */\n  theme?: 'light' | 'dark';\n  /** Additional wrapper classes. */\n  className?: string;\n}\nexport function CooldownNotice({\n  cooldownUntil,\n  labels,\n  theme,\n  className = '',\n}: CooldownNoticeProps) {\n  const [now, setNow] = useState<number | null>(null);\n  useEffect(() => {\n    setNow(Date.now());\n    const timer = setInterval(() => setNow(Date.now()), 1000);\n    return () => clearInterval(timer);\n  }, [cooldownUntil]);\n  const deadline = Date.parse(cooldownUntil);\n  const valid = Number.isFinite(deadline);\n  const seconds = now === null || !valid ? null : Math.max(0, Math.ceil((deadline - now) / 1000));\n  const ready = seconds === 0;\n  const countdown =\n    seconds === null\n      ? '—:—'\n      : (labels?.countdown?.(seconds) ??\n        `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, '0')}`);\n  return (\n    <section\n      data-knock-item=\"cooldown\"\n      data-theme={theme}\n      className={`rounded-xl border border-[var(--knock-border,var(--knock-line))] bg-[var(--knock-bg,var(--knock-surface))] p-6 ${className}`}\n    >\n      <style>{`[data-knock-item]{--knock-surface:#fffefa;--knock-text:#30382c;--knock-secondary:#65705b;--knock-line:#d8dece;--knock-highlight:#a3462d;color:var(--knock-ink,var(--knock-text));color-scheme:light}[data-knock-item][data-theme=dark]{--knock-surface:#293028;--knock-text:#f0f3e9;--knock-secondary:#b6c2ab;--knock-line:#59654f;--knock-highlight:#f1a187;color-scheme:dark}@media(prefers-color-scheme:dark){[data-knock-item]:not([data-theme=light]){--knock-surface:#293028;--knock-text:#f0f3e9;--knock-secondary:#b6c2ab;--knock-line:#59654f;--knock-highlight:#f1a187;color-scheme:dark}}[data-knock-item] a:focus-visible,[data-knock-item] button:focus-visible,[data-knock-item] [tabindex]:focus-visible{outline:2px solid var(--knock-accent,var(--knock-highlight));outline-offset:4px}[data-knock-item] button,[data-knock-item] a{overflow-wrap:anywhere}@media(prefers-reduced-motion:reduce){[data-knock-item] *{animation:none!important;transition:none!important}}`}</style>\n      <div className=\"mb-4 flex items-center justify-between gap-4\">\n        <svg\n          aria-hidden=\"true\"\n          width=\"25\"\n          height=\"25\"\n          viewBox=\"0 0 24 24\"\n          fill=\"none\"\n          stroke=\"currentColor\"\n          strokeWidth=\"1.5\"\n          className=\"text-2xl text-[var(--knock-accent,var(--knock-highlight))]\"\n        >\n          <path d=\"M4 9a8 8 0 1 1 0 6M4 3v6h6\" />\n        </svg>\n        <span role=\"timer\" aria-live=\"off\" className=\"font-mono text-2xl tabular-nums\">\n          {countdown}\n        </span>\n      </div>\n      <h3 className=\"text-lg font-medium\">{labels?.heading ?? 'A little breathing room.'}</h3>\n      <p className=\"mt-2 text-sm leading-relaxed text-[var(--knock-muted,var(--knock-secondary))]\">\n        {labels?.description ??\n          'A few tries didn’t quite match. Take a moment, then try the code from your email again.'}\n      </p>\n      <p role=\"status\" aria-live=\"polite\" className=\"mt-4 text-sm font-medium\">\n        {!valid\n          ? (labels?.unavailable ?? 'Retry time is unavailable. Please contact the preview owner.')\n          : ready\n            ? (labels?.ready ?? 'Ready when you are. You can try again.')\n            : (labels?.waiting ?? 'Your preview will be right here.')}\n      </p>\n    </section>\n  );\n}\n"
    }
  ],
  "meta": {
    "version": "0.2.0",
    "lifecycle": "gate",
    "whenToUse": "A retry window is running and the gate should stay on screen, with the wait stated calmly beside the code field.",
    "notFor": "A full screen that replaces the gate during the pause. Use the Cooldown screen block.",
    "pitfalls": [
      "Reaching zero announces readiness and nothing else. It never submits, never re-enables your submit button and never clears the cooldown.",
      "`cooldownUntil` is a timestamp you own. A value already in the past renders the ready state immediately; the component counts no attempts."
    ],
    "dependencies": [],
    "usedBy": [
      "client-preview-gate",
      "cooldown-screen",
      "expired-notice",
      "gated-section",
      "open-invitation",
      "preview-chrome",
      "quick-gate",
      "revoked-notice",
      "teaser-gate"
    ],
    "sourceHash": "e89ed4f48be96b2c9ea9490c2b32bc55405db2b04306727971952fe8f5525c3c",
    "component": "CooldownNotice",
    "contract": "Displays a calm countdown without enforcing a retry policy. Does not verify codes, persist state, or grant or revoke access. Contract: the consumer owns lifecycle timestamps and action callbacks.",
    "props": [
      {
        "name": "cooldownUntil",
        "type": "string",
        "required": true,
        "default": null,
        "description": "Retry deadline as ISO 8601 with timezone; required. Invalid values show unavailable text."
      },
      {
        "name": "labels",
        "type": "{ heading?: string; description?: string; ready?: string; waiting?: string; unavailable?: string; countdown?: (seconds: number) => string; }",
        "required": false,
        "default": null,
        "description": "Localized visible text and countdown formatter.",
        "properties": [
          {
            "name": "heading",
            "type": "string",
            "required": false,
            "default": "'A little breathing room.'"
          },
          {
            "name": "description",
            "type": "string",
            "required": false,
            "default": "'A few tries didn’t quite match. Take a moment, then try the code from your email again.'"
          },
          {
            "name": "ready",
            "type": "string",
            "required": false,
            "default": "'Ready when you are. You can try again.'"
          },
          {
            "name": "waiting",
            "type": "string",
            "required": false,
            "default": "'Your preview will be right here.'"
          },
          {
            "name": "unavailable",
            "type": "string",
            "required": false,
            "default": "'Retry time is unavailable. Please contact the preview owner.'"
          },
          {
            "name": "countdown",
            "type": "(seconds: number) => string",
            "required": false,
            "default": null
          }
        ]
      },
      {
        "name": "theme",
        "type": "'light' | 'dark'",
        "required": false,
        "default": null,
        "description": "Explicit theme; omitted follows the system preference."
      },
      {
        "name": "className",
        "type": "string",
        "required": false,
        "default": "''",
        "description": "Additional wrapper classes."
      }
    ],
    "defaults": {
      "className": "''",
      "labels.heading": "'A little breathing room.'",
      "labels.description": "'A few tries didn’t quite match. Take a moment, then try the code from your email again.'",
      "labels.unavailable": "'Retry time is unavailable. Please contact the preview owner.'",
      "labels.ready": "'Ready when you are. You can try again.'",
      "labels.waiting": "'Your preview will be right here.'"
    },
    "slots": [],
    "a11y": "Native controls, visible focus, localized labels; lifecycle messages use polite announcements. No modal dialog is created.",
    "unsupportedProps": [
      "onSuccess",
      "password",
      "correctCode",
      "attempts",
      "maxAttempts",
      "onUnlock"
    ],
    "examples": {
      "minimal": "'use client';\nimport { useState } from 'react';\nimport { CooldownNotice } from '@/components/knock/cooldown-notice';\n\nexport default function Example() {\n  const [value, setValue] = useState('');\n  const [unlocked, setUnlocked] = useState(false);\n  const [message, setMessage] = useState('');\n  return <section aria-label=\"Cooldown notice example\">\n    {/* Demo consumer state only. Integrate your authorization outside this UI. */}\n    <CooldownNotice cooldownUntil=\"2030-01-01T12:00:00Z\" />\n    <p role=\"status\">{message}</p>\n  </section>;\n}\n",
      "full": "'use client';\nimport { useState } from 'react';\nimport { CooldownNotice } from '@/components/knock/cooldown-notice';\n\nexport default function Example() {\n  const [value, setValue] = useState('');\n  const [unlocked, setUnlocked] = useState(false);\n  const [message, setMessage] = useState('');\n  return <section aria-label=\"Cooldown notice example\">\n    {/* Demo consumer state only. Integrate your authorization outside this UI. */}\n    <CooldownNotice cooldownUntil=\"2030-01-01T12:00:00Z\" theme=\"light\" labels={{ heading: \"Take a moment.\", description: \"Your preview will be here.\", waiting: \"Please wait a little.\", ready: \"You can try again.\", unavailable: \"Ask the preview owner.\", countdown: seconds => `${seconds}s` }} />\n    <p role=\"status\">{message}</p>\n  </section>;\n}\n"
    }
  },
  "docs": "UI only. Read https://knock.codes/docs/cooldown-notice.md. Consumers own authorization."
}
