{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "login-2",
  "title": "Login 2: split screen with testimonial",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A split-screen sign-in: SSO and passkey buttons, validated email and password with show/hide, remember me and error state on one side, a soft colour mesh with a customer quote on the other.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/button.json",
    "https://ui.ballmac.com/r/checkbox.json",
    "https://ui.ballmac.com/r/field.json",
    "https://ui.ballmac.com/r/input.json",
    "https://ui.ballmac.com/r/password-input.json",
    "https://ui.ballmac.com/r/media.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/blocks/login-2/login-2.tsx",
      "content": "// Ballmac UI: Login 2. https://ui.ballmac.com/blocks/login-2\n\"use client\"\n\nimport * as React from \"react\"\nimport { ArrowRight, Fingerprint, KeyRound, Quote } from \"lucide-react\"\n\nimport { Button } from \"@/components/ballmac/button\"\nimport { Checkbox } from \"@/components/ballmac/checkbox\"\nimport { Field, FieldError, FieldGroup, FieldLabel, useFieldControl } from \"@/components/ballmac/field\"\nimport { Input } from \"@/components/ballmac/input\"\nimport { PasswordInput } from \"@/components/ballmac/password-input\"\nimport { cn } from \"@/lib/utils\"\nimport { Media, type MediaSource } from \"@/components/ballmac/media\"\n\ntype Login2Values = { email: string; password: string; remember: boolean }\n\ntype Login2Props = Omit<React.ComponentProps<\"section\">, \"title\" | \"onSubmit\"> & {\n  /** Brand name beside the logo mark. */\n  brand?: string\n  /** Your logo mark instead of the brand's first letter: an image URL, an object with alt text and a dark-mode file, or your own element. A plain URL is described by the brand name. */\n  logo?: MediaSource\n  /** Heading. */\n  title?: string\n  /** One sentence under the heading. */\n  description?: string\n  /** Called with the values once they are valid. Throw to show `errorMessage`; resolve to finish. */\n  onSubmit?: (values: Login2Values) => void | Promise<void>\n  /** Called by \"Continue with SSO\". Pass null to hide the button. */\n  onSso?: (() => void) | null\n  /** Called by \"Use a passkey\". Pass null to hide the button. */\n  onPasskey?: (() => void) | null\n  /** Shown above the form when onSubmit throws. */\n  errorMessage?: string\n  /** Where \"Forgot password?\" goes. */\n  forgotHref?: string\n  /** Where \"Create an account\" goes. */\n  signupHref?: string\n  /** Quote on the picture side. */\n  quote?: string\n  /** Who said it. */\n  author?: { name: string; role: string }\n  /** Replaces the picture side with your own element. Hidden below the lg breakpoint. */\n  visual?: React.ReactNode\n  /** A photo or illustration for the picture side: an image URL (give it imageAlt), an object with alt text and a dark-mode file, or your own element. */\n  image?: MediaSource\n  /** Describes `image` when it is a plain URL. */\n  imageAlt?: string\n}\n\nfunction FieldInput(props: React.ComponentProps<typeof Input>) {\n  return <Input {...useFieldControl()} {...props} />\n}\nfunction FieldPassword(props: React.ComponentProps<typeof PasswordInput>) {\n  return <PasswordInput {...useFieldControl()} {...props} />\n}\n\nconst emailPattern = /^\\S+@\\S+\\.\\S+$/\n\nfunction Login2({\n  brand = \"Acme\",\n  logo,\n  title = \"Welcome back\",\n  description = \"Sign in to pick up where you left off.\",\n  onSubmit,\n  onSso = () => {},\n  onPasskey = () => {},\n  errorMessage = \"That email and password don’t match. Try again, or reset your password.\",\n  forgotHref = \"#\",\n  signupHref = \"#\",\n  quote = \"We moved our entire finance team over in an afternoon. The calm, fast interface is the reason people actually use it.\",\n  author = { name: \"Priya Raman\", role: \"VP Finance, Northwind\" },\n  visual,\n  image,\n  imageAlt,\n  className,\n  ...props\n}: Login2Props) {\n  const [email, setEmail] = React.useState(\"\")\n  const [password, setPassword] = React.useState(\"\")\n  const [remember, setRemember] = React.useState(true)\n  const [touched, setTouched] = React.useState({ email: false, password: false })\n  const [state, setState] = React.useState<\"idle\" | \"sending\" | \"error\">(\"idle\")\n  const emailError = touched.email && !emailPattern.test(email) ? \"Enter a full email address, like name@company.com.\" : undefined\n  const passwordError = touched.password && !password ? \"Enter your password.\" : undefined\n\n  async function submit(e: React.FormEvent) {\n    e.preventDefault()\n    setTouched({ email: true, password: true })\n    if (!emailPattern.test(email) || !password) return\n    setState(\"sending\")\n    try {\n      if (onSubmit) await onSubmit({ email, password, remember })\n      else await new Promise((r) => setTimeout(r, 800))\n      setState(\"idle\")\n    } catch {\n      setState(\"error\")\n    }\n  }\n\n  return (\n    <section data-slot=\"login-2\" className={cn(\"grid min-h-[44rem] lg:grid-cols-2\", className)} {...props}>\n      <div className=\"flex items-center justify-center px-4 py-14 sm:px-8\">\n        <div className=\"w-full max-w-sm\">\n          <a href=\"#\" className=\"focus-visible:ring-ring/50 inline-flex items-center gap-2.5 rounded-md text-lg font-semibold tracking-tight outline-none focus-visible:ring-[3px]\">\n            {logo ? <Media media={logo} alt={brand} aspect=\"square\" fit=\"contain\" priority className=\"size-7 shrink-0 rounded-lg\" /> : <span aria-hidden=\"true\" className=\"bg-foreground text-background flex size-7 items-center justify-center rounded-lg text-sm font-bold\">{brand[0]}</span>}\n            {brand}\n          </a>\n          <h1 className=\"mt-10 text-3xl font-semibold tracking-[-0.035em]\">{title}</h1>\n          <p className=\"text-muted-foreground mt-2 text-pretty\">{description}</p>\n\n          {(onSso || onPasskey) && (\n            <div className=\"mt-8 grid gap-2 sm:grid-cols-2\">\n              {onSso && <Button variant=\"outline\" onClick={onSso}><KeyRound /> Continue with SSO</Button>}\n              {onPasskey && <Button variant=\"outline\" onClick={onPasskey}><Fingerprint /> Use a passkey</Button>}\n            </div>\n          )}\n          <div className=\"text-muted-foreground my-6 flex items-center gap-3 text-xs\" aria-hidden=\"true\">\n            <span className=\"bg-border h-px flex-1\" />\n            or continue with email\n            <span className=\"bg-border h-px flex-1\" />\n          </div>\n\n          <form noValidate onSubmit={submit} aria-label=\"Sign in\">\n            <FieldGroup>\n              {state === \"error\" && <p role=\"alert\" className=\"border-destructive/40 text-destructive rounded-lg border px-3 py-2.5 text-sm\">{errorMessage}</p>}\n              <Field invalid={!!emailError}>\n                <FieldLabel>Email</FieldLabel>\n                <FieldInput type=\"email\" autoComplete=\"email\" placeholder=\"you@company.com\" value={email} onChange={(e) => setEmail(e.target.value)} onBlur={() => setTouched((t) => ({ ...t, email: true }))} />\n                <FieldError errors={[emailError]} />\n              </Field>\n              <Field invalid={!!passwordError}>\n                <div className=\"flex items-center justify-between\">\n                  <FieldLabel>Password</FieldLabel>\n                  <a href={forgotHref} className=\"text-muted-foreground hover:text-foreground focus-visible:ring-ring/50 rounded-sm text-sm underline-offset-4 outline-none hover:underline focus-visible:ring-[3px]\">Forgot password?</a>\n                </div>\n                <FieldPassword showStrength={false} autoComplete=\"current-password\" value={password} onValueChange={setPassword} onBlur={() => setTouched((t) => ({ ...t, password: true }))} />\n                <FieldError errors={[passwordError]} />\n              </Field>\n              <Field orientation=\"horizontal\">\n                <Checkbox id=\"login-2-remember\" checked={remember} onCheckedChange={(c) => setRemember(c === true)} />\n                <FieldLabel htmlFor=\"login-2-remember\" className=\"font-normal\">Keep me signed in</FieldLabel>\n              </Field>\n              <Button type=\"submit\" size=\"lg\" shape=\"pill\" loading={state === \"sending\"} className=\"w-full\">\n                Sign in <ArrowRight  className=\"rtl:rotate-180\"/>\n              </Button>\n            </FieldGroup>\n          </form>\n\n          <p className=\"text-muted-foreground mt-8 text-center text-sm\">\n            New to {brand}?{\" \"}\n            <a href={signupHref} className=\"text-foreground focus-visible:ring-ring/50 rounded-sm font-medium underline underline-offset-4 outline-none focus-visible:ring-[3px]\">Create an account</a>\n          </p>\n        </div>\n      </div>\n\n      <div className=\"bg-muted/40 relative hidden overflow-hidden border-s lg:block\" aria-hidden={visual || image ? undefined : true}>\n        {image ? <Media media={image} alt={imageAlt} fill priority className=\"absolute inset-0\" /> : visual ?? (\n          <>\n            <div className=\"absolute inset-0\">\n              <div className=\"bg-chart-1/25 absolute -top-24 -end-16 size-[30rem] rounded-full blur-[100px]\" />\n              <div className=\"bg-chart-3/25 absolute top-1/2 -start-24 size-[26rem] rounded-full blur-[100px]\" />\n              <div className=\"bg-chart-5/20 absolute -bottom-24 end-1/4 size-[24rem] rounded-full blur-[100px]\" />\n              <div className=\"absolute inset-0 bg-[radial-gradient(color-mix(in_oklch,var(--foreground)_14%,transparent)_1px,transparent_1px)] [background-size:22px_22px] [mask-image:radial-gradient(70%_70%_at_50%_50%,black,transparent)]\" />\n            </div>\n            <div className=\"relative flex h-full flex-col justify-between p-12 xl:p-16\">\n              <div className=\"bg-card/80 ms-auto w-64 rounded-2xl border p-4 shadow-[0_24px_60px_-30px_rgb(0_0_0/0.4)] backdrop-blur-xl\">\n                <p className=\"text-muted-foreground text-xs\">Paid this week</p>\n                <p className=\"mt-1 text-2xl font-semibold tracking-tight\">$128,430</p>\n                <div className=\"mt-3 flex h-10 items-end gap-1\">\n                  {[30, 48, 40, 62, 55, 78, 92].map((h, i) => (\n                    <span key={i} className={cn(\"flex-1 rounded-t\", i === 6 ? \"bg-chart-1\" : \"bg-chart-1/30\")} style={{ height: `${h}%` }} />\n                  ))}\n                </div>\n              </div>\n              <figure className=\"bg-card/80 max-w-lg rounded-3xl border p-8 shadow-[0_30px_80px_-40px_rgb(0_0_0/0.45)] backdrop-blur-xl\">\n                <Quote className=\"text-chart-1 size-8 rtl:-scale-x-100\" />\n                <blockquote className=\"mt-4 text-xl leading-snug font-medium tracking-[-0.02em] text-balance\">{quote}</blockquote>\n                <figcaption className=\"mt-6 flex items-center gap-3\">\n                  <span className=\"bg-chart-1/25 flex size-10 items-center justify-center rounded-full text-sm font-semibold\">{author.name.split(\" \").map((w) => w[0]).join(\"\")}</span>\n                  <span className=\"text-sm\"><span className=\"block font-medium\">{author.name}</span><span className=\"text-muted-foreground\">{author.role}</span></span>\n                </figcaption>\n              </figure>\n            </div>\n          </>\n        )}\n      </div>\n    </section>\n  )\n}\n\nexport { Login2, type Login2Props, type Login2Values }\n",
      "type": "registry:component",
      "target": "@components/ballmac/blocks/login-2/login-2.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "login",
      "sign in",
      "auth",
      "split screen",
      "passkey",
      "sso"
    ],
    "version": "1.0.0",
    "updated": "2026-10-01",
    "ai": {
      "summary": "A full-width sign-in page. Pass onSubmit(values) (throw to show errorMessage), onSso / onPasskey (null hides), quote and author, or visual to replace the right side.",
      "whenToUse": [
        "Product sign-in pages with room for a brand moment",
        "Apps with SSO and passkeys"
      ],
      "whenNotToUse": [
        "Small centered dialogs (use login-1)"
      ],
      "composesWith": [
        "signup-1",
        "forgot-password-1",
        "verify-1"
      ],
      "a11y": [
        {
          "keys": "Submit with errors",
          "action": "Each error is announced and linked to its field; a failed sign-in is announced as an alert"
        },
        {
          "keys": "Show / hide button",
          "action": "Toggles the password with aria-pressed"
        }
      ],
      "customization": [
        "onSubmit(values) async; throw for the error state",
        "quote, author or visual (any node)",
        "onSso / onPasskey: null hides"
      ]
    },
    "examples": [
      "login-2-demo",
      "login-2-error"
    ],
    "url": "https://ui.ballmac.com/components/login-2"
  },
  "categories": [
    "blocks",
    "auth"
  ],
  "type": "registry:block"
}