{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "sign-in-card",
  "title": "Sign-in card",
  "description": "One card for getting in: email a sign-in link or continue with GitHub, then a check-your-inbox state; in waitlist mode, a short join form whose confirmation sprouts.",
  "registryDependencies": [
    "https://sproutboat.com/ikada/r/theme.json",
    "https://sproutboat.com/ikada/r/button.json",
    "https://sproutboat.com/ikada/r/cn.json",
    "https://sproutboat.com/ikada/r/field.json",
    "https://sproutboat.com/ikada/r/icons.json",
    "https://sproutboat.com/ikada/r/input.json",
    "https://sproutboat.com/ikada/r/select.json",
    "https://sproutboat.com/ikada/r/status-glyph.json",
    "https://sproutboat.com/ikada/r/textarea.json"
  ],
  "files": [
    {
      "path": "registry/ikada/blocks/sign-in-card/sign-in-card.tsx",
      "content": "\"use client\";\n\n/**\n * One card for getting in: email a sign-in link or continue with GitHub, then a check-your-inbox\n * state; in waitlist mode, a short join form whose confirmation sprouts.\n */\nimport { useState } from \"react\";\nimport type { FormEvent, ReactNode } from \"react\";\nimport { Button } from \"@/components/ikada/button\";\nimport { Field } from \"@/components/ikada/field\";\nimport { Input } from \"@/components/ikada/input\";\nimport { Select } from \"@/components/ikada/select\";\nimport { StatusGlyph } from \"@/components/ikada/status-glyph\";\nimport { Textarea } from \"@/components/ikada/textarea\";\nimport { cn, focusRing } from \"@/components/ikada/lib/cn\";\nimport { GitHubIcon, MailIcon } from \"@/components/ikada/lib/icons\";\n\nexport type SignInMode = \"sign-in\" | \"waitlist\";\n\nexport interface WaitlistEntry {\n  email: string;\n  useCase: string;\n  note?: string;\n}\n\nexport interface SignInCardProps {\n  /** Controlled mode. Leave unset to let the card's own links switch between sign-in and waitlist. */\n  mode?: SignInMode;\n  defaultMode?: SignInMode;\n  onModeChange?: (mode: SignInMode) => void;\n  /** The product's mark and name, at the top of the card. */\n  brand?: ReactNode;\n  /** Short line under the heading in sign-in mode. */\n  note?: string;\n  /** Send the sign-in link. Throw to show an error under the field. */\n  onSendLink?: (email: string) => void | Promise<void>;\n  onGitHub?: () => void;\n  /** Save the waitlist entry. Throw to show an error. */\n  onJoinWaitlist?: (entry: WaitlistEntry) => void | Promise<void>;\n  useCases?: { value: string; label: string }[];\n  className?: string;\n}\n\nconst EMAIL = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\n\n/** Sample data: what people say they will build. */\nexport const sampleUseCases = [\n  { value: \"side-project\", label: \"A side project\" },\n  { value: \"startup\", label: \"A startup's product\" },\n  { value: \"work\", label: \"Something at work\" },\n  { value: \"learning\", label: \"Learning and trying things\" },\n];\n\n/** A sample backend: answers after a short wait, like a real request. */\nconst wait = () => new Promise<void>((resolve) => setTimeout(resolve, 700));\n\nfunction SproutMark() {\n  return (\n    <svg viewBox=\"3 6 24 25\" className=\"size-6 shrink-0 text-brand-dot\" fill=\"none\" aria-hidden=\"true\">\n      <path d=\"M16 29V15\" stroke=\"currentColor\" strokeWidth=\"2.6\" strokeLinecap=\"round\" />\n      <path\n        d=\"M16 16.5c0-4.7-3.8-8.5-8.5-8.5C6.7 8 6 8.7 6 9.5 6 14.2 9.8 18 14.5 18c.8 0 1.5-.7 1.5-1.5Z\"\n        fill=\"currentColor\"\n      />\n      <path\n        d=\"M16 15c0-4 3.2-7.2 7.2-7.2.7 0 1.3.6 1.3 1.3C24.5 12.8 21.3 16 17.3 16c-.7 0-1.3-.6-1.3-1Z\"\n        fill=\"currentColor\"\n        fillOpacity=\"0.72\"\n      />\n    </svg>\n  );\n}\n\nconst sampleBrand = (\n  <span className=\"inline-flex items-center gap-2 text-sb-md font-bold\">\n    <SproutMark />\n    Sproutboat\n  </span>\n);\n\n/** An inline text button that switches modes, styled as a link. */\nfunction SwitchLink({ onClick, children }: { onClick: () => void; children: ReactNode }) {\n  return (\n    <button\n      type=\"button\"\n      onClick={onClick}\n      className={cn(\"rounded-sm font-semibold text-fg underline underline-offset-2 hover:text-brand-fg\", focusRing)}\n    >\n      {children}\n    </button>\n  );\n}\n\nfunction SignIn({\n  note,\n  onSendLink,\n  onGitHub,\n  toWaitlist,\n}: {\n  note: string;\n  onSendLink: (email: string) => void | Promise<void>;\n  onGitHub?: () => void;\n  toWaitlist: () => void;\n}) {\n  const [email, setEmail] = useState(\"\");\n  const [error, setError] = useState<string | null>(null);\n  const [sending, setSending] = useState(false);\n  const [sent, setSent] = useState(false);\n\n  async function submit(event: FormEvent) {\n    event.preventDefault();\n    if (!EMAIL.test(email.trim())) {\n      setError(\"Enter the email address you were invited with.\");\n      return;\n    }\n    setSending(true);\n    setError(null);\n    try {\n      await onSendLink(email.trim());\n      setSent(true);\n    } catch (caught) {\n      setError(caught instanceof Error && caught.message ? caught.message : \"The link did not send. Try again.\");\n    } finally {\n      setSending(false);\n    }\n  }\n\n  if (sent) {\n    return (\n      <div className=\"flex flex-col gap-5\" role=\"status\">\n        <span\n          className=\"grid size-10 place-items-center rounded-lg bg-well text-fg-muted shadow-tray\"\n          aria-hidden=\"true\"\n        >\n          <MailIcon className=\"size-5\" />\n        </span>\n        <div className=\"flex flex-col gap-2\">\n          <h2 className=\"text-sb-xl font-bold\">Check your inbox</h2>\n          <p className=\"text-sb-base text-fg-muted\">\n            A sign-in link is on its way to <span className=\"font-semibold break-all text-fg\">{email.trim()}</span>. It\n            works once and expires in 15 minutes.\n          </p>\n        </div>\n        <Button variant=\"ghost\" onClick={() => setSent(false)}>\n          Use a different email\n        </Button>\n      </div>\n    );\n  }\n\n  return (\n    <form onSubmit={submit} noValidate className=\"flex flex-col gap-6\">\n      <div className=\"flex flex-col gap-1.5\">\n        <h2 className=\"text-sb-xl font-bold\">Sign in</h2>\n        <p className=\"text-sb-base text-fg-muted\">{note}</p>\n      </div>\n      <div className=\"flex flex-col gap-3\">\n        <Field label=\"Email\" error={error ?? undefined}>\n          <Input\n            type=\"email\"\n            name=\"email\"\n            autoComplete=\"email\"\n            inputMode=\"email\"\n            value={email}\n            onChange={(event) => setEmail(event.target.value)}\n            placeholder=\"you@example.com\"\n          />\n        </Field>\n        <Button type=\"submit\" variant=\"primary\" loading={sending}>\n          Email me a sign-in link\n        </Button>\n      </div>\n      <div className=\"flex items-center gap-3 text-sb-sm text-fg-muted\" aria-hidden=\"true\">\n        <span className=\"h-px flex-1 bg-hairline\" />\n        or\n        <span className=\"h-px flex-1 bg-hairline\" />\n      </div>\n      <Button type=\"button\" variant=\"secondary\" onClick={onGitHub}>\n        <GitHubIcon />\n        Continue with GitHub\n      </Button>\n      <p className=\"text-center text-sb-sm text-fg-muted\">\n        No invite yet? <SwitchLink onClick={toWaitlist}>Join the waitlist</SwitchLink>\n      </p>\n    </form>\n  );\n}\n\nfunction Waitlist({\n  useCases,\n  onJoin,\n  toSignIn,\n}: {\n  useCases: { value: string; label: string }[];\n  onJoin: (entry: WaitlistEntry) => void | Promise<void>;\n  toSignIn: () => void;\n}) {\n  const [email, setEmail] = useState(\"\");\n  const [useCase, setUseCase] = useState<string | null>(null);\n  const [note, setNote] = useState(\"\");\n  const [errors, setErrors] = useState<{ email?: string; useCase?: string; form?: string }>({});\n  const [saving, setSaving] = useState(false);\n  const [done, setDone] = useState<string | null>(null);\n\n  async function submit(event: FormEvent) {\n    event.preventDefault();\n    const next: typeof errors = {};\n    if (!EMAIL.test(email.trim())) next.email = \"Enter an email address we can write to.\";\n    if (!useCase) next.useCase = \"Pick the closest one.\";\n    setErrors(next);\n    if (next.email || !useCase) return;\n    setSaving(true);\n    try {\n      await onJoin({ email: email.trim(), useCase, note: note.trim() || undefined });\n      setDone(email.trim());\n    } catch {\n      setErrors({ form: \"That did not save. Check your connection and try again.\" });\n    } finally {\n      setSaving(false);\n    }\n  }\n\n  if (done) {\n    return (\n      <div className=\"flex flex-col gap-4\" role=\"status\">\n        <span className=\"grid size-10 place-items-center rounded-full bg-brand-soft\" aria-hidden=\"true\">\n          <StatusGlyph tone=\"success\" sprout className=\"size-3.5\" />\n        </span>\n        <h2 className=\"text-sb-xl font-bold\">You are on the list</h2>\n        <p className=\"text-sb-base text-fg-muted\">\n          We let people in a few at a time and will write to{\" \"}\n          <span className=\"font-semibold break-all text-fg\">{done}</span> when your spot opens. Nothing else.\n        </p>\n        <p className=\"text-sb-sm text-fg-muted\">\n          Invited after all? <SwitchLink onClick={toSignIn}>Sign in</SwitchLink>\n        </p>\n      </div>\n    );\n  }\n\n  return (\n    <form onSubmit={submit} noValidate className=\"flex flex-col gap-5\">\n      <div className=\"flex flex-col gap-1.5\">\n        <h2 className=\"text-sb-xl font-bold\">Join the waitlist</h2>\n        <p className=\"text-sb-base text-fg-muted\">Tell us a little, and we will invite you when a spot opens.</p>\n      </div>\n      <Field label=\"Email\" error={errors.email}>\n        <Input\n          type=\"email\"\n          autoComplete=\"email\"\n          inputMode=\"email\"\n          value={email}\n          onChange={(event) => setEmail(event.target.value)}\n          placeholder=\"you@example.com\"\n        />\n      </Field>\n      <Field label=\"What will you build?\" error={errors.useCase}>\n        <Select\n          value={useCase}\n          placeholder=\"Choose one\"\n          options={useCases}\n          invalid={Boolean(errors.useCase)}\n          onValueChange={(value) => setUseCase(typeof value === \"string\" ? value : null)}\n        />\n      </Field>\n      <Field label=\"Anything we should know?\" description=\"Optional. Your framework, or what you are missing today.\">\n        <Textarea value={note} onChange={(event) => setNote(event.target.value)} rows={3} maxLength={500} />\n      </Field>\n      {errors.form ? <p className=\"text-sb-sm text-danger-fg\">{errors.form}</p> : null}\n      <Button type=\"submit\" variant=\"primary\" loading={saving}>\n        Join the waitlist\n      </Button>\n      <p className=\"text-center text-sb-sm text-fg-muted\">\n        Already invited? <SwitchLink onClick={toSignIn}>Sign in</SwitchLink>\n      </p>\n    </form>\n  );\n}\n\nexport function SignInCard({\n  mode: controlled,\n  defaultMode = \"sign-in\",\n  onModeChange,\n  brand = sampleBrand,\n  note = \"Invite-only while we are in alpha.\",\n  onSendLink = wait,\n  onGitHub,\n  onJoinWaitlist = wait,\n  useCases = sampleUseCases,\n  className,\n}: SignInCardProps) {\n  const [own, setOwn] = useState<SignInMode>(defaultMode);\n  const mode = controlled ?? own;\n  const go = (next: SignInMode) => {\n    setOwn(next);\n    onModeChange?.(next);\n  };\n  return (\n    <div\n      className={cn(\n        \"mx-auto flex w-full max-w-sm flex-col gap-6 rounded-xl bg-surface p-6 text-fg shadow-card\",\n        className,\n      )}\n    >\n      {brand ? <div>{brand}</div> : null}\n      {mode === \"sign-in\" ? (\n        <SignIn note={note} onSendLink={onSendLink} onGitHub={onGitHub} toWaitlist={() => go(\"waitlist\")} />\n      ) : (\n        <Waitlist useCases={useCases} onJoin={onJoinWaitlist} toSignIn={() => go(\"sign-in\")} />\n      )}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "@components/ikada/blocks/sign-in-card/sign-in-card.tsx"
    }
  ],
  "categories": [
    "dashboard"
  ],
  "type": "registry:block"
}