{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"login-form","type":"registry:block","title":"Login Form","description":"Sign-in card that pairs with Signup Form — email, password and remember-me with generic SSO/passkey buttons — whose \"Forgot password?\" flow morphs inside the same card from requesting a code to a check-your-inbox screen with a resend cooldown, then an OTP plus new-password step and a drawn success check, with every async handler driving pending states and shaking the card on rejection.","author":"kara <ztzyrant@gmail.com>","dependencies":["clsx","lucide-react","motion","tailwind-merge"],"registryDependencies":[],"files":[{"path":"components/motion/login-form.tsx","type":"registry:component","target":"@components/motion/login-form.tsx","content":"\"use client\";\n// karaui.kamisama-v0.my.id/components/blocks/login-form\n\nimport {\n  ArrowLeft,\n  Building2,\n  Fingerprint,\n  type LucideIcon,\n  Mail,\n  MailCheck,\n} from \"lucide-react\";\nimport {\n  AnimatePresence,\n  animate,\n  motion,\n  useReducedMotion,\n} from \"motion/react\";\nimport {\n  type FormEvent,\n  type ReactNode,\n  useCallback,\n  useEffect,\n  useId,\n  useRef,\n  useState,\n} from \"react\";\nimport { Button, StatefulButton } from \"@/components/motion/button\";\nimport { Checkbox } from \"@/components/motion/checkbox\";\nimport { Input } from \"@/components/motion/input\";\nimport { OTPInput } from \"@/components/motion/otp-input\";\nimport { PasswordInput } from \"@/components/motion/password-input\";\nimport { EASE_OUT, SPRING_LAYOUT, SPRING_SWAP } from \"@/lib/ease\";\nimport { MIN_PASSWORD_LENGTH } from \"@/lib/password-strength\";\nimport { cn } from \"@/lib/utils\";\n\n/** The card's screens, in the order the recovery flow walks them. */\nexport type LoginView = \"sign-in\" | \"forgot\" | \"sent\" | \"reset\" | \"done\";\n\nexport type LoginValues = {\n  email: string;\n  password: string;\n  remember: boolean;\n};\n\nexport type LoginResetValues = {\n  email: string;\n  code: string;\n  password: string;\n};\n\nexport type LoginSocialProvider = {\n  id: string;\n  label: string;\n  /** Any icon component. Generic glyphs by default — bring your own brand marks. */\n  icon?: LucideIcon;\n};\n\nexport interface LoginFormProps {\n  /** Controlled screen. Omit to let the card manage its own flow. */\n  view?: LoginView;\n  defaultView?: LoginView;\n  onViewChange?: (view: LoginView) => void;\n  defaultValues?: Partial<LoginValues>;\n  /** Called with valid values. Reject (or throw) with a message to show it and shake the card. */\n  onSubmit?: (values: LoginValues) => void | Promise<void>;\n  /** Social / SSO buttons. Pass `[]` to hide the row. */\n  providers?: LoginSocialProvider[];\n  onProviderClick?: (id: string) => void | Promise<void>;\n  /** Sends the reset code. Reject to show an error on the request screen. */\n  onRequestReset?: (email: string) => void | Promise<void>;\n  /** Verifies the code and stores the new password. Reject to shake the code. */\n  onResetPassword?: (values: LoginResetValues) => void | Promise<void>;\n  /** Length of the emailed code. Default 6. */\n  codeLength?: number;\n  /** Seconds before \"Resend code\" unlocks again. Default 30. */\n  resendCooldown?: number;\n  title?: ReactNode;\n  description?: ReactNode;\n  footer?: ReactNode;\n  className?: string;\n}\n\ntype Status = \"idle\" | \"loading\" | \"success\" | \"error\";\n\nconst DEFAULT_PROVIDERS: LoginSocialProvider[] = [\n  { id: \"sso\", label: \"SSO\", icon: Building2 },\n  { id: \"passkey\", label: \"Passkey\", icon: Fingerprint },\n];\n\n// Same deliberately permissive check as SignUpForm.\nconst EMAIL_PATTERN = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;\nconst SHAKE = { x: [0, -8, 8, -5, 5, -2, 0] };\nconst ORDER: LoginView[] = [\"sign-in\", \"forgot\", \"sent\", \"reset\", \"done\"];\n\nfunction messageOf(reason: unknown, fallback: string) {\n  if (reason instanceof Error && reason.message) return reason.message;\n  if (typeof reason === \"string\" && reason) return reason;\n  return fallback;\n}\n\nfunction emailError(email: string) {\n  if (!email.trim()) return \"Enter your email.\";\n  if (!EMAIL_PATTERN.test(email)) return \"That doesn't look like an email address.\";\n  return undefined;\n}\n\n/**\n * Sign-in card, sibling of `SignUpForm`: email + password with remember-me and\n * generic social buttons, plus a password-recovery flow that morphs inside the\n * same card — request a code, \"check your inbox\", OTP + new password, success.\n * Every handler may return a promise; rejections surface their message and\n * shake the active screen.\n */\nexport function LoginForm({\n  view: viewProp,\n  defaultView = \"sign-in\",\n  onViewChange,\n  defaultValues,\n  onSubmit,\n  providers = DEFAULT_PROVIDERS,\n  onProviderClick,\n  onRequestReset,\n  onResetPassword,\n  codeLength = 6,\n  resendCooldown = 30,\n  title = \"Welcome back\",\n  description = \"Sign in to continue to your workspace.\",\n  footer,\n  className,\n}: LoginFormProps) {\n  const reduce = useReducedMotion();\n  const baseId = useId();\n\n  const [internalView, setInternalView] = useState<LoginView>(defaultView);\n  const view = viewProp ?? internalView;\n  const [direction, setDirection] = useState<1 | -1>(1);\n\n  const [email, setEmail] = useState(defaultValues?.email ?? \"\");\n  const [password, setPassword] = useState(defaultValues?.password ?? \"\");\n  const [remember, setRemember] = useState(defaultValues?.remember ?? true);\n  const [touched, setTouched] = useState<{ email?: boolean; password?: boolean }>(\n    {},\n  );\n\n  const [code, setCode] = useState(\"\");\n  const [newPassword, setNewPassword] = useState(\"\");\n  const [resetTouched, setResetTouched] = useState(false);\n\n  const [status, setStatus] = useState<Status>(\"idle\");\n  const [error, setError] = useState<string>();\n  const [providerPending, setProviderPending] = useState<string | null>(null);\n  const [cooldown, setCooldown] = useState(0);\n\n  const shakeRef = useRef<HTMLDivElement>(null);\n  const headingRef = useRef<HTMLHeadingElement>(null);\n  const shownView = useRef(view);\n\n  const go = useCallback(\n    (next: LoginView) => {\n      // Finishing the reset loops forward to sign-in; every other move slides\n      // by its place in the flow.\n      setDirection(\n        view === \"done\" || ORDER.indexOf(next) >= ORDER.indexOf(view) ? 1 : -1,\n      );\n      setStatus(\"idle\");\n      setError(undefined);\n      if (viewProp === undefined) setInternalView(next);\n      onViewChange?.(next);\n    },\n    [view, viewProp, onViewChange],\n  );\n\n  // Move focus to the new screen's heading so keyboard users land on it.\n  useEffect(() => {\n    // Only after navigation, never on first render.\n    if (shownView.current === view) return;\n    shownView.current = view;\n    const id = window.setTimeout(\n      () => headingRef.current?.focus({ preventScroll: true }),\n      60,\n    );\n    return () => window.clearTimeout(id);\n  }, [view]);\n\n  useEffect(() => {\n    if (cooldown <= 0) return;\n    const id = window.setTimeout(() => setCooldown((s) => s - 1), 1000);\n    return () => window.clearTimeout(id);\n  }, [cooldown]);\n\n  const shake = () => {\n    if (reduce || !shakeRef.current) return;\n    animate(shakeRef.current, SHAKE, { duration: 0.42, ease: EASE_OUT });\n  };\n\n  /** Shared async runner: pending → success | error (+ shake). */\n  const run = async (\n    task: (() => void | Promise<void>) | undefined,\n    fallback: string,\n  ) => {\n    setError(undefined);\n    setStatus(\"loading\");\n    try {\n      await task?.();\n      setStatus(\"success\");\n      return true;\n    } catch (reason) {\n      setStatus(\"error\");\n      setError(messageOf(reason, fallback));\n      shake();\n      return false;\n    }\n  };\n\n  const signInErrors = {\n    email: emailError(email),\n    password: password ? undefined : \"Enter your password.\",\n  };\n\n  const handleSignIn = async (event: FormEvent<HTMLFormElement>) => {\n    event.preventDefault();\n    setTouched({ email: true, password: true });\n    if (signInErrors.email || signInErrors.password) {\n      shake();\n      return;\n    }\n    await run(\n      onSubmit && (() => onSubmit({ email, password, remember })),\n      \"Couldn't sign you in. Try again.\",\n    );\n  };\n\n  const handleRequest = async (event: FormEvent<HTMLFormElement>) => {\n    event.preventDefault();\n    setTouched((t) => ({ ...t, email: true }));\n    if (emailError(email)) {\n      shake();\n      return;\n    }\n    const ok = await run(\n      onRequestReset && (() => onRequestReset(email)),\n      \"Couldn't send the code. Try again.\",\n    );\n    if (ok) {\n      setCooldown(resendCooldown);\n      go(\"sent\");\n    }\n  };\n\n  const handleResend = async () => {\n    if (cooldown > 0) return;\n    try {\n      await onRequestReset?.(email);\n      setCooldown(resendCooldown);\n    } catch (reason) {\n      setError(messageOf(reason, \"Couldn't resend the code.\"));\n      shake();\n    }\n  };\n\n  const resetErrors = {\n    code: code.length < codeLength ? `Enter the ${codeLength}-digit code.` : undefined,\n    password:\n      newPassword.length < MIN_PASSWORD_LENGTH\n        ? `Use at least ${MIN_PASSWORD_LENGTH} characters.`\n        : undefined,\n  };\n\n  const handleReset = async (event: FormEvent<HTMLFormElement>) => {\n    event.preventDefault();\n    setResetTouched(true);\n    if (resetErrors.code || resetErrors.password) {\n      shake();\n      return;\n    }\n    const ok = await run(\n      onResetPassword &&\n        (() => onResetPassword({ email, code, password: newPassword })),\n      \"That code didn't work.\",\n    );\n    if (ok) {\n      setPassword(\"\");\n      setCode(\"\");\n      setNewPassword(\"\");\n      setResetTouched(false);\n      go(\"done\");\n    } else {\n      setCode(\"\");\n    }\n  };\n\n  const handleProvider = async (id: string) => {\n    if (providerPending) return;\n    setProviderPending(id);\n    setError(undefined);\n    try {\n      await onProviderClick?.(id);\n    } catch (reason) {\n      setError(messageOf(reason, \"That sign-in method is unavailable.\"));\n      shake();\n    } finally {\n      setProviderPending(null);\n    }\n  };\n\n  const busy = status === \"loading\";\n  const errorId = `${baseId}-error`;\n  const offset = reduce ? 0 : 28;\n\n  const heading = (text: ReactNode, sub: ReactNode) => (\n    <div className=\"flex flex-col gap-1\">\n      <h2\n        ref={headingRef}\n        tabIndex={-1}\n        className=\"text-xl font-semibold tracking-tight text-foreground outline-none\"\n      >\n        {text}\n      </h2>\n      {sub ? <p className=\"text-sm text-muted-foreground\">{sub}</p> : null}\n    </div>\n  );\n\n  const back = (to: LoginView, label = \"Back to sign in\") => (\n    <button\n      type=\"button\"\n      onClick={() => go(to)}\n      disabled={busy}\n      className=\"-ml-1 inline-flex w-fit items-center gap-1.5 rounded-full px-1 text-xs font-medium text-muted-foreground outline-none transition-colors hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50\"\n    >\n      <ArrowLeft className=\"size-3.5\" aria-hidden />\n      {label}\n    </button>\n  );\n\n  const formError = (\n    <AnimatePresence initial={false}>\n      {error ? (\n        <motion.p\n          id={errorId}\n          role=\"alert\"\n          initial={reduce ? { opacity: 0 } : { opacity: 0, y: -4 }}\n          animate={{ opacity: 1, y: 0 }}\n          exit={reduce ? { opacity: 0 } : { opacity: 0, y: -4 }}\n          transition={{ duration: 0.2, ease: EASE_OUT }}\n          className=\"rounded-2xl border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive\"\n        >\n          {error}\n        </motion.p>\n      ) : null}\n    </AnimatePresence>\n  );\n\n  let screen: ReactNode;\n  switch (view) {\n    case \"sign-in\":\n      screen = (\n        <form noValidate onSubmit={handleSignIn} className=\"flex flex-col gap-5\">\n          {heading(title, description)}\n          <div className=\"flex flex-col gap-4\">\n            <Input\n              label=\"Email\"\n              type=\"email\"\n              inputMode=\"email\"\n              autoComplete=\"email\"\n              placeholder=\"you@example.com\"\n              leftIcon={<Mail />}\n              disabled={busy}\n              value={email}\n              onChange={(next) => {\n                setEmail(next);\n                if (status === \"error\") setStatus(\"idle\");\n              }}\n              onBlur={() => setTouched((t) => ({ ...t, email: true }))}\n              error={touched.email ? signInErrors.email : undefined}\n            />\n            <PasswordInput\n              label=\"Password\"\n              autoComplete=\"current-password\"\n              placeholder=\"Your password\"\n              strengthMeter={false}\n              disabled={busy}\n              value={password}\n              onChange={(next) => {\n                setPassword(next);\n                if (status === \"error\") setStatus(\"idle\");\n              }}\n              onBlur={() => setTouched((t) => ({ ...t, password: true }))}\n              error={touched.password ? signInErrors.password : undefined}\n            />\n          </div>\n          <div className=\"flex items-center justify-between gap-3\">\n            <Checkbox\n              checked={remember}\n              disabled={busy}\n              onCheckedChange={setRemember}\n              label=\"Remember me\"\n            />\n            <button\n              type=\"button\"\n              disabled={busy}\n              onClick={() => go(\"forgot\")}\n              className=\"rounded-full px-1 text-sm font-medium text-foreground underline-offset-4 outline-none hover:underline focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50\"\n            >\n              Forgot password?\n            </button>\n          </div>\n          {formError}\n          <StatefulButton\n            type=\"submit\"\n            size=\"lg\"\n            state={status}\n            loadingText=\"Signing in\"\n            successText=\"Signed in\"\n            errorText=\"Try again\"\n            aria-describedby={error ? errorId : undefined}\n            className=\"w-full\"\n          >\n            Sign in\n          </StatefulButton>\n          {providers.length > 0 ? (\n            <>\n              <div className=\"flex items-center gap-3 text-xs text-muted-foreground\">\n                <span className=\"h-px flex-1 bg-border\" aria-hidden />\n                or continue with\n                <span className=\"h-px flex-1 bg-border\" aria-hidden />\n              </div>\n              <div\n                className={cn(\n                  \"grid gap-2\",\n                  providers.length > 1 ? \"grid-cols-2\" : \"grid-cols-1\",\n                )}\n              >\n                {providers.map((provider) => {\n                  const Icon = provider.icon;\n                  const pending = providerPending === provider.id;\n                  return (\n                    <Button\n                      key={provider.id}\n                      type=\"button\"\n                      variant=\"secondary\"\n                      disabled={busy || Boolean(providerPending)}\n                      aria-busy={pending || undefined}\n                      onClick={() => void handleProvider(provider.id)}\n                      className=\"w-full\"\n                    >\n                      {pending ? (\n                        <span\n                          aria-hidden\n                          className=\"size-4 animate-spin rounded-full border-2 border-foreground/20 border-t-foreground\"\n                        />\n                      ) : Icon ? (\n                        <Icon className=\"size-4\" aria-hidden />\n                      ) : null}\n                      {provider.label}\n                    </Button>\n                  );\n                })}\n              </div>\n            </>\n          ) : null}\n          {footer ? (\n            <div className=\"text-center text-sm text-muted-foreground\">{footer}</div>\n          ) : null}\n        </form>\n      );\n      break;\n    case \"forgot\":\n      screen = (\n        <form noValidate onSubmit={handleRequest} className=\"flex flex-col gap-5\">\n          {back(\"sign-in\")}\n          {heading(\n            \"Reset your password\",\n            \"Enter your account email and we'll send you a reset code.\",\n          )}\n          <Input\n            label=\"Email\"\n            type=\"email\"\n            inputMode=\"email\"\n            autoComplete=\"email\"\n            placeholder=\"you@example.com\"\n            leftIcon={<Mail />}\n            disabled={busy}\n            value={email}\n            onChange={setEmail}\n            onBlur={() => setTouched((t) => ({ ...t, email: true }))}\n            error={touched.email ? emailError(email) : undefined}\n          />\n          {formError}\n          <StatefulButton\n            type=\"submit\"\n            size=\"lg\"\n            state={status}\n            loadingText=\"Sending code\"\n            successText=\"Code sent\"\n            errorText=\"Try again\"\n            className=\"w-full\"\n          >\n            Send reset code\n          </StatefulButton>\n        </form>\n      );\n      break;\n    case \"sent\":\n      screen = (\n        <div className=\"flex flex-col items-center gap-5 text-center\">\n          <motion.span\n            initial={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.6, y: 8 }}\n            animate={{ opacity: 1, scale: 1, y: 0 }}\n            transition={reduce ? { duration: 0.2 } : { ...SPRING_SWAP, delay: 0.08 }}\n            className=\"grid size-14 place-items-center rounded-2xl bg-muted text-foreground\"\n          >\n            <MailCheck className=\"size-6\" aria-hidden />\n          </motion.span>\n          {heading(\n            \"Check your inbox\",\n            <>\n              We sent a {codeLength}-digit code to{\" \"}\n              <span className=\"font-medium text-foreground\">{email}</span>.\n            </>,\n          )}\n          {formError}\n          <Button type=\"button\" size=\"lg\" className=\"w-full\" onClick={() => go(\"reset\")}>\n            Enter code\n          </Button>\n          <div className=\"flex w-full items-center justify-between text-sm\">\n            {back(\"forgot\", \"Use another email\")}\n            <button\n              type=\"button\"\n              disabled={cooldown > 0}\n              onClick={() => void handleResend()}\n              className=\"rounded-full px-1 font-medium text-foreground tabular-nums outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:text-muted-foreground\"\n            >\n              {cooldown > 0 ? `Resend in ${cooldown}s` : \"Resend code\"}\n            </button>\n          </div>\n        </div>\n      );\n      break;\n    case \"reset\":\n      screen = (\n        <form noValidate onSubmit={handleReset} className=\"flex flex-col gap-5\">\n          {back(\"sent\")}\n          {heading(\"Choose a new password\", `Code sent to ${email}.`)}\n          <OTPInput\n            length={codeLength}\n            label=\"Reset code\"\n            value={code}\n            onChange={(next) => {\n              setCode(next);\n              if (status === \"error\") {\n                setStatus(\"idle\");\n                setError(undefined);\n              }\n            }}\n            disabled={busy}\n            status={\n              status === \"error\"\n                ? \"error\"\n                : resetTouched && resetErrors.code\n                  ? \"error\"\n                  : \"idle\"\n            }\n            errorMessage={\n              status === \"error\" ? undefined : resetTouched ? resetErrors.code : undefined\n            }\n          />\n          <PasswordInput\n            label=\"New password\"\n            autoComplete=\"new-password\"\n            placeholder={`At least ${MIN_PASSWORD_LENGTH} characters`}\n            disabled={busy}\n            value={newPassword}\n            onChange={setNewPassword}\n            error={resetTouched ? resetErrors.password : undefined}\n          />\n          {formError}\n          <StatefulButton\n            type=\"submit\"\n            size=\"lg\"\n            state={status}\n            loadingText=\"Updating\"\n            successText=\"Updated\"\n            errorText=\"Try again\"\n            className=\"w-full\"\n          >\n            Reset password\n          </StatefulButton>\n        </form>\n      );\n      break;\n    case \"done\":\n      screen = (\n        <div className=\"flex flex-col items-center gap-5 text-center\">\n          <motion.span\n            initial={reduce ? { opacity: 0 } : { opacity: 0, scale: 0.6 }}\n            animate={{ opacity: 1, scale: 1 }}\n            transition={reduce ? { duration: 0.2 } : SPRING_SWAP}\n            className=\"grid size-14 place-items-center rounded-full bg-success/15 text-success\"\n          >\n            <svg viewBox=\"0 0 24 24\" className=\"size-7\" fill=\"none\" aria-hidden=\"true\">\n              <motion.path\n                d=\"M5 12.5l4.5 4.5L19 7.5\"\n                stroke=\"currentColor\"\n                strokeWidth={2.4}\n                strokeLinecap=\"round\"\n                strokeLinejoin=\"round\"\n                initial={{ pathLength: reduce ? 1 : 0 }}\n                animate={{ pathLength: 1 }}\n                transition={{ duration: 0.4, delay: 0.15, ease: EASE_OUT }}\n              />\n            </svg>\n          </motion.span>\n          {heading(\n            \"Password updated\",\n            \"You can now sign in with your new password.\",\n          )}\n          <Button type=\"button\" size=\"lg\" className=\"w-full\" onClick={() => go(\"sign-in\")}>\n            Back to sign in\n          </Button>\n        </div>\n      );\n      break;\n  }\n\n  return (\n    // `layout` animates the card's size between screens — a layout job by\n    // design (FLIP transforms under the hood); the screens counter-correct\n    // with layout=\"position\" so their content never stretches.\n    <motion.div\n      layout\n      transition={reduce ? { duration: 0 } : SPRING_LAYOUT}\n      style={{ borderRadius: 24 }}\n      className={cn(\n        \"w-full max-w-sm overflow-hidden border border-border bg-card p-6\",\n        className,\n      )}\n    >\n      <div ref={shakeRef}>\n        <AnimatePresence mode=\"popLayout\" initial={false} custom={direction}>\n          <motion.div\n            key={view}\n            layout=\"position\"\n            custom={direction}\n            variants={{\n              enter: (dir: number) => ({\n                opacity: 0,\n                x: dir * offset,\n                filter: reduce ? \"blur(0px)\" : \"blur(4px)\",\n              }),\n              center: {\n                opacity: 1,\n                x: 0,\n                filter: \"blur(0px)\",\n                transition: { duration: 0.28, ease: EASE_OUT },\n              },\n              exit: (dir: number) => ({\n                opacity: 0,\n                x: dir * -offset,\n                filter: reduce ? \"blur(0px)\" : \"blur(4px)\",\n                transition: { duration: 0.16, ease: EASE_OUT },\n              }),\n            }}\n            initial=\"enter\"\n            animate=\"center\"\n            exit=\"exit\"\n          >\n            {screen}\n          </motion.div>\n        </AnimatePresence>\n      </div>\n    </motion.div>\n  );\n}\n"},{"path":"components/motion/button/index.tsx","type":"registry:component","target":"@components/motion/button/index.tsx","content":"export type {\n  ButtonLinkProps,\n  ButtonProps,\n  ButtonSize,\n  ButtonVariant,\n} from \"./base\";\nexport { Button, ButtonLink } from \"./base\";\nexport type { MagneticButtonProps } from \"./magnetic\";\nexport { MagneticButton } from \"./magnetic\";\nexport type { ButtonState, StatefulButtonProps } from \"./stateful\";\nexport { StatefulButton } from \"./stateful\";\n"},{"path":"components/motion/checkbox.tsx","type":"registry:component","target":"@components/motion/checkbox.tsx","content":"\"use client\";\n\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport { useId } from \"react\";\nimport { EASE_OUT, SPRING_PRESS } from \"@/lib/ease\";\nimport { TOUCH_TARGET_CLASS } from \"@/lib/touch\";\nimport { cn } from \"@/lib/utils\";\n\nconst CHECK_PATH = \"M5 13l4 4L19 7\";\nconst INDETERMINATE_PATH = \"M6 12h12\";\n\nexport interface CheckboxProps {\n  checked: boolean;\n  onCheckedChange: (checked: boolean) => void;\n  disabled?: boolean;\n  indeterminate?: boolean;\n  label?: string;\n  className?: string;\n  id?: string;\n  \"aria-label\"?: string;\n  /** Associates an external message (e.g. a form error) with the control. */\n  \"aria-describedby\"?: string;\n}\n\nexport function Checkbox({\n  checked,\n  onCheckedChange,\n  disabled,\n  indeterminate,\n  label,\n  className,\n  id: idProp,\n  \"aria-label\": ariaLabel,\n  \"aria-describedby\": ariaDescribedBy,\n}: CheckboxProps) {\n  const autoId = useId();\n  const id = idProp ?? autoId;\n  const reduce = useReducedMotion();\n  const showMark = checked || indeterminate;\n  const path = indeterminate ? INDETERMINATE_PATH : CHECK_PATH;\n\n  return (\n    <label\n      htmlFor={id}\n      className={cn(\n        \"inline-flex items-center gap-3\",\n        disabled ? \"cursor-not-allowed\" : \"cursor-pointer\",\n        className,\n      )}\n    >\n      <motion.button\n        id={id}\n        type=\"button\"\n        role=\"checkbox\"\n        aria-checked={indeterminate ? \"mixed\" : checked}\n        aria-label={ariaLabel}\n        aria-describedby={ariaDescribedBy}\n        disabled={disabled}\n        onClick={() => !disabled && onCheckedChange(!checked)}\n        whileTap={reduce || disabled ? undefined : { scale: 0.92 }}\n        transition={SPRING_PRESS}\n        data-state={\n          checked ? \"checked\" : indeterminate ? \"indeterminate\" : \"unchecked\"\n        }\n        className={cn(\n          \"relative inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-md border-2 outline-none transition-colors duration-200\",\n          TOUCH_TARGET_CLASS,\n          \"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n          \"disabled:cursor-not-allowed disabled:opacity-60\",\n          showMark\n            ? \"border-primary bg-primary text-primary-foreground\"\n            : \"border-muted-foreground/50 bg-background hover:border-muted-foreground\",\n        )}\n      >\n        <AnimatePresence initial={false}>\n          {showMark ? (\n            <motion.svg\n              key={indeterminate ? \"indeterminate\" : \"checked\"}\n              width=\"12\"\n              height=\"12\"\n              viewBox=\"0 0 24 24\"\n              fill=\"none\"\n              stroke=\"currentColor\"\n              strokeWidth={3}\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n              initial={reduce ? { opacity: 1 } : { opacity: 0, scale: 0.5 }}\n              animate={reduce ? { opacity: 1 } : { opacity: 1, scale: 1 }}\n              exit={\n                reduce\n                  ? { opacity: 0 }\n                  : { opacity: 0, scale: 0.5, filter: \"blur(4px)\" }\n              }\n              transition={\n                reduce ? { duration: 0 } : { duration: 0.16, ease: EASE_OUT }\n              }\n              aria-hidden\n            >\n              <title>{indeterminate ? \"Partially selected\" : \"Selected\"}</title>\n              <motion.path\n                d={path}\n                initial={reduce ? { pathLength: 1 } : { pathLength: 0 }}\n                animate={{ pathLength: 1 }}\n                transition={\n                  reduce\n                    ? { duration: 0 }\n                    : {\n                        duration: indeterminate ? 0.2 : 0.3,\n                        ease: EASE_OUT,\n                        delay: 0.04,\n                      }\n                }\n              />\n            </motion.svg>\n          ) : null}\n        </AnimatePresence>\n      </motion.button>\n      {label ? (\n        <span className={cn(\"select-none text-sm text-foreground\", disabled && \"opacity-60\")}>\n          {label}\n        </span>\n      ) : null}\n    </label>\n  );\n}\n"},{"path":"components/motion/input.tsx","type":"registry:component","target":"@components/motion/input.tsx","content":"\"use client\";\n\nimport {\n  AnimatePresence,\n  animate,\n  motion,\n  useReducedMotion,\n} from \"motion/react\";\nimport {\n  forwardRef,\n  useEffect,\n  useId,\n  useRef,\n  useState,\n  type InputHTMLAttributes,\n  type ReactNode,\n} from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport type InputClassNames = {\n  root?: string;\n  label?: string;\n  field?: string;\n  input?: string;\n  leftIcon?: string;\n  rightIcon?: string;\n  successIcon?: string;\n  errorMessage?: string;\n};\n\nexport interface InputProps extends Omit<\n  InputHTMLAttributes<HTMLInputElement>,\n  \"value\" | \"defaultValue\" | \"onChange\"\n> {\n  label?: string;\n  value?: string;\n  defaultValue?: string;\n  onChange?: (value: string) => void;\n  /** Truthy error triggers a shake, red border and (if a string) a message. */\n  error?: string | boolean;\n  success?: boolean;\n  leftIcon?: ReactNode;\n  rightIcon?: ReactNode;\n  className?: string;\n  classNames?: InputClassNames;\n}\n\nexport const Input = forwardRef<HTMLInputElement, InputProps>(function Input(\n  {\n    label,\n    value: valueProp,\n    defaultValue,\n    onChange,\n    onFocus,\n    onBlur,\n    error,\n    success,\n    leftIcon,\n    rightIcon,\n    className,\n    classNames,\n    disabled,\n    id: idProp,\n    type,\n    ...rest\n  },\n  ref,\n) {\n  const reactId = useId();\n  const id = idProp ?? reactId;\n  const reduce = useReducedMotion();\n\n  const controlled = valueProp !== undefined;\n  const [internal, setInternal] = useState(defaultValue ?? \"\");\n  const value = controlled ? (valueProp ?? \"\") : internal;\n\n  const [focused, setFocused] = useState(false);\n\n  const fieldRef = useRef<HTMLDivElement>(null);\n\n  const hasError = Boolean(error);\n  const errorMessage = typeof error === \"string\" ? error : null;\n\n  // Right edge shows the success check, otherwise the caller's right icon.\n  const rightSlot = success ? null : rightIcon;\n\n  // Shake the field when an error appears.\n  useEffect(() => {\n    if (!fieldRef.current || reduce || !hasError) return;\n    animate(\n      fieldRef.current,\n      { x: [0, -6, 6, -4, 4, -2, 0] },\n      { duration: 0.45 },\n    );\n  }, [hasError, reduce]);\n\n  const handleChange = (next: string) => {\n    if (!controlled) setInternal(next);\n    onChange?.(next);\n  };\n\n  return (\n    <div\n      className={cn(\"flex flex-col gap-1.5\", className, classNames?.root)}\n    >\n      {label ? (\n        <label\n          htmlFor={id}\n          className={cn(\n            \"px-1 text-sm font-medium text-foreground\",\n            classNames?.label,\n          )}\n        >\n          {label}\n        </label>\n      ) : null}\n\n      <div\n        ref={fieldRef}\n        data-state={\n          hasError\n            ? \"error\"\n            : success\n              ? \"success\"\n              : focused\n                ? \"focused\"\n                : \"idle\"\n        }\n        className={cn(\n          \"relative h-11 overflow-hidden rounded-full border transition-colors duration-200\",\n          \"border-border\",\n          focused && !hasError && \"border-foreground/40 ring-2 ring-ring/40\",\n          hasError && \"border-destructive ring-2 ring-destructive/25\",\n          disabled && \"opacity-60\",\n          classNames?.field,\n        )}\n      >\n        {leftIcon ? (\n          <span\n            className={cn(\n              \"pointer-events-none absolute left-3 top-1/2 flex -translate-y-1/2 items-center text-muted-foreground [&_svg]:h-4 [&_svg]:w-4\",\n              classNames?.leftIcon,\n            )}\n          >\n            {leftIcon}\n          </span>\n        ) : null}\n\n        <input\n          ref={ref}\n          id={id}\n          type={type}\n          value={value}\n          disabled={disabled}\n          aria-invalid={hasError || undefined}\n          aria-describedby={errorMessage ? `${id}-error` : undefined}\n          {...rest}\n          onChange={(e) => handleChange(e.target.value)}\n          onFocus={(event) => {\n            setFocused(true);\n            onFocus?.(event);\n          }}\n          onBlur={(event) => {\n            setFocused(false);\n            onBlur?.(event);\n          }}\n          className={cn(\n            \"peer h-full w-full bg-transparent text-base leading-6 text-foreground caret-foreground outline-none\",\n            \"placeholder:text-muted-foreground/60\",\n            leftIcon ? \"pl-10\" : \"pl-3.5\",\n            rightSlot || success ? \"pr-10\" : \"pr-3.5\",\n            disabled && \"cursor-not-allowed\",\n            classNames?.input,\n          )}\n        />\n\n        {success ? (\n          <motion.svg\n            viewBox=\"0 0 24 24\"\n            fill=\"none\"\n            className={cn(\n              \"absolute right-3.5 top-1/2 h-5 w-5 -translate-y-1/2 text-(--color-success)\",\n              classNames?.successIcon,\n            )}\n          >\n            <motion.path\n              d=\"M5 12.5l4.5 4.5L19 7.5\"\n              stroke=\"currentColor\"\n              strokeWidth={2.5}\n              strokeLinecap=\"round\"\n              strokeLinejoin=\"round\"\n              initial={reduce ? { pathLength: 1 } : { pathLength: 0 }}\n              animate={{ pathLength: 1 }}\n              transition={{ duration: 0.35, ease: \"easeOut\" }}\n            />\n          </motion.svg>\n        ) : rightSlot ? (\n          <span\n            className={cn(\n              \"absolute right-0 top-0 flex h-full items-center text-muted-foreground [&_button]:grid [&_button]:size-11 [&_button]:place-items-center [&_svg]:h-4 [&_svg]:w-4\",\n              classNames?.rightIcon,\n            )}\n          >\n            {rightSlot}\n          </span>\n        ) : null}\n      </div>\n\n      <AnimatePresence initial={false}>\n        {errorMessage ? (\n          <motion.p\n            id={`${id}-error`}\n            role=\"alert\"\n            initial={\n              reduce\n                ? { opacity: 0 }\n                : { opacity: 0, y: -4, filter: \"blur(4px)\" }\n            }\n            animate={{ opacity: 1, y: 0, filter: \"blur(0px)\" }}\n            exit={\n              reduce\n                ? { opacity: 0 }\n                : { opacity: 0, y: -4, filter: \"blur(4px)\" }\n            }\n            transition={{ duration: 0.2 }}\n            className={cn(\n              \"px-1 text-xs text-destructive\",\n              classNames?.errorMessage,\n            )}\n          >\n            {errorMessage}\n          </motion.p>\n        ) : null}\n      </AnimatePresence>\n    </div>\n  );\n});\n"},{"path":"components/motion/otp-input.tsx","type":"registry:component","target":"@components/motion/otp-input.tsx","content":"\"use client\";\n\nimport {\n  AnimatePresence,\n  animate,\n  motion,\n  useReducedMotion,\n} from \"motion/react\";\nimport { useEffect, useId, useRef, useState } from \"react\";\nimport { EASE_OUT } from \"@/lib/ease\";\nimport { cn } from \"@/lib/utils\";\n\nexport type OTPStatus = \"idle\" | \"error\" | \"success\";\n\nexport interface OTPInputProps {\n  /** Number of slots. Default 6. */\n  length?: number;\n  value?: string;\n  defaultValue?: string;\n  onChange?: (value: string) => void;\n  /** Fires once every slot is filled. */\n  onComplete?: (value: string) => void;\n  /** Optional label rendered above the slots. */\n  label?: string;\n  /** Helper text shown below the slots while idle. */\n  hint?: string;\n  /** Message shown below the slots when status is \"success\". */\n  successMessage?: string;\n  /** Message shown below the slots when status is \"error\". */\n  errorMessage?: string;\n  /** External validation feedback. \"error\" shakes, \"success\" draws a check. */\n  status?: OTPStatus;\n  /** Render dots instead of the typed digits. */\n  mask?: boolean;\n  disabled?: boolean;\n  autoFocus?: boolean;\n  /** Accessible label for the underlying input. */\n  \"aria-label\"?: string;\n  className?: string;\n}\n\nexport function OTPInput({\n  length = 6,\n  value: controlledValue,\n  defaultValue = \"\",\n  onChange,\n  onComplete,\n  label,\n  hint,\n  successMessage,\n  errorMessage,\n  status = \"idle\",\n  mask = false,\n  disabled = false,\n  autoFocus = false,\n  \"aria-label\": ariaLabel = \"One-time passcode\",\n  className,\n}: OTPInputProps) {\n  const uid = useId();\n  const reduce = useReducedMotion();\n  const inputRef = useRef<HTMLInputElement>(null);\n  const slotsRef = useRef<HTMLDivElement>(null);\n\n  const controlled = controlledValue !== undefined;\n\n  // Source of truth is a fixed-length array, so a cleared middle slot stays an\n  // in-place hole instead of collapsing the digits after it to the left.\n  const [slots, setSlots] = useState<string[]>(() =>\n    toSlots(controlled ? controlledValue : defaultValue, length),\n  );\n  const [focused, setFocused] = useState(false);\n  const [active, setActive] = useState(0);\n\n  const joined = slots.join(\"\");\n  const joinedRef = useRef(joined);\n\n  useEffect(() => {\n    joinedRef.current = joined;\n  }, [joined]);\n\n  // Pull in external value changes; skip when the parent is just echoing our own\n  // onChange, so internal holes survive the controlled round-trip.\n  useEffect(() => {\n    if (!controlled) return;\n    const incoming = sanitize(controlledValue, length);\n    if (incoming !== joinedRef.current) setSlots(toSlots(incoming, length));\n  }, [controlled, controlledValue, length]);\n\n  const commit = (next: string[]) => {\n    const wasComplete = slots.every((c) => c !== \"\");\n    setSlots(next);\n    const str = next.join(\"\");\n    onChange?.(str);\n    // Fire only on the empty→full transition, not on every edit of a full code.\n    if (!wasComplete && next.every((c) => c !== \"\")) onComplete?.(str);\n  };\n\n  const clearSlot = (idx: number) => {\n    const next = [...slots];\n    next[idx] = \"\";\n    commit(next);\n  };\n\n  const slotFromClientX = (clientX: number) => {\n    const els = slotsRef.current?.children;\n    if (!els) return 0;\n    for (let i = 0; i < els.length; i++) {\n      if (clientX < els[i].getBoundingClientRect().right) return i;\n    }\n    return length - 1;\n  };\n\n  // Single insertion path: one digit overwrites the active slot and advances; a\n  // multi-digit chunk (paste / SMS autofill) fills forward from the active slot.\n  const insert = (raw: string, from = active) => {\n    const digits = raw.replace(/\\D/g, \"\");\n    if (!digits) return;\n    const next = [...slots];\n    let i = from;\n    for (const ch of digits) {\n      if (i >= length) break;\n      next[i] = ch;\n      i++;\n    }\n    commit(next);\n    setActive(Math.min(i, length - 1));\n  };\n\n  // The keyboard is the single source of truth: preventDefault on keydown\n  // reliably blocks native insertion (unlike beforeinput in React), so the hidden\n  // input never accumulates a competing string and slot holes survive.\n  const onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {\n    if (disabled || e.metaKey || e.ctrlKey || e.altKey) return;\n    const k = e.key;\n    if (/^[0-9]$/.test(k)) {\n      e.preventDefault();\n      insert(k);\n    } else if (k === \"Backspace\") {\n      e.preventDefault();\n      // A filled slot clears in place; an empty slot steps back and clears there.\n      if (slots[active]) {\n        clearSlot(active);\n      } else if (active > 0) {\n        clearSlot(active - 1);\n        setActive((current) => Math.max(current - 1, 0));\n      }\n    } else if (k === \"Delete\") {\n      e.preventDefault();\n      clearSlot(active);\n    } else if (k === \"ArrowLeft\") {\n      e.preventDefault();\n      setActive((a) => Math.max(a - 1, 0));\n    } else if (k === \"ArrowRight\") {\n      e.preventDefault();\n      setActive((a) => Math.min(a + 1, length - 1));\n    } else if (k === \"Home\") {\n      e.preventDefault();\n      setActive(0);\n    } else if (k === \"End\") {\n      e.preventDefault();\n      setActive(length - 1);\n    }\n  };\n\n  const onPaste = (e: React.ClipboardEvent<HTMLInputElement>) => {\n    if (disabled) return;\n    // preventDefault suppresses the duplicate onChange, keeping that path autofill-only.\n    e.preventDefault();\n    insert(e.clipboardData.getData(\"text\"), active);\n  };\n\n  // Autofill path: SMS one-time-code arrives as a whole value in one shot.\n  // Keystrokes go through onKeyDown and paste through onPaste, so only autofill\n  // reaches here — spread it across the slots from the start.\n  const onChangeNative = (e: React.ChangeEvent<HTMLInputElement>) => {\n    const digits = sanitize(e.target.value, length);\n    if (!digits) return;\n    commit(toSlots(digits, length));\n    setActive(Math.min(digits.length, length - 1));\n  };\n\n  // Error shake — imperative so it replays on every transition into \"error\".\n  useEffect(() => {\n    if (status !== \"error\" || reduce || !slotsRef.current) return;\n    animate(\n      slotsRef.current,\n      { x: [0, -5, 5, -3, 3, -1, 0] },\n      { duration: 0.45, ease: EASE_OUT },\n    );\n  }, [status, reduce]);\n\n  const showSuccess = status === \"success\";\n  const activeIndex = focused ? active : -1;\n  const message = showSuccess\n    ? successMessage\n    : status === \"error\"\n      ? errorMessage\n      : hint;\n\n  return (\n    <div className={cn(\"inline-flex flex-col gap-2\", className)}>\n      {label ? (\n        <label\n          htmlFor={`${uid}-input`}\n          className=\"text-sm font-medium text-foreground\"\n        >\n          {label}\n        </label>\n      ) : null}\n      <fieldset\n        className=\"relative m-0 inline-flex w-max border-0 p-0\"\n        onMouseDown={(e) => {\n          if (disabled) return;\n          // Suppress the native click-caret; we drive the active slot ourselves.\n          e.preventDefault();\n          // Clamp to the first empty slot so a click can't jump ahead of progress.\n          const firstEmpty = slots.indexOf(\"\");\n          const cap = firstEmpty === -1 ? length - 1 : firstEmpty;\n          setActive(Math.min(slotFromClientX(e.clientX), cap));\n          inputRef.current?.focus();\n        }}\n      >\n        <input\n          ref={inputRef}\n          id={`${uid}-input`}\n          inputMode=\"numeric\"\n          autoComplete=\"one-time-code\"\n          // biome-ignore lint/a11y/noAutofocus: opt-in via prop for OTP-first screens.\n          autoFocus={autoFocus}\n          disabled={disabled}\n          aria-label={ariaLabel}\n          aria-invalid={status === \"error\"}\n          // Kept empty on purpose — our state owns the digits, native holds none.\n          value=\"\"\n          maxLength={length}\n          onKeyDown={onKeyDown}\n          onChange={onChangeNative}\n          onPaste={onPaste}\n          onFocus={() => setFocused(true)}\n          onBlur={() => setFocused(false)}\n          // Transparent overlay owns focus, the soft keyboard, paste and autofill;\n          // the slots below are purely presentational.\n          className=\"absolute inset-0 z-20 h-full w-full cursor-text bg-transparent text-transparent caret-transparent opacity-0 outline-none disabled:cursor-not-allowed\"\n        />\n\n        <div ref={slotsRef} className=\"flex items-center gap-2\">\n          {Array.from({ length }, (_, i) => {\n            const char = slots[i] ?? \"\";\n            const isActive = i === activeIndex;\n            return (\n              <div\n                // biome-ignore lint/suspicious/noArrayIndexKey: fixed-length slot grid, never reordered.\n                key={`${uid}-${i}`}\n                data-active={isActive}\n                data-filled={char !== \"\"}\n                className={cn(\n                  \"relative grid h-14 w-12 place-items-center overflow-hidden rounded-xl border text-xl font-semibold tabular-nums transition-colors duration-200\",\n                  showSuccess\n                    ? \"border-emerald-500/60 text-foreground\"\n                    : status === \"error\"\n                      ? \"border-destructive/60 text-foreground\"\n                      : char\n                        ? \"border-border-strong text-foreground\"\n                        : \"border-border text-muted-foreground\",\n                  // Active slot reads stronger; twMerge lets this win the border.\n                  isActive && !showSuccess && status !== \"error\" && \"border-foreground\",\n                  disabled && \"opacity-50\",\n                )}\n              >\n                {/* Blinking caret marks the active slot — centered when empty,\n                    trailing the digit when the slot is already filled. */}\n                {isActive && !showSuccess ? (\n                  <motion.span\n                    aria-hidden\n                    animate={reduce ? undefined : { opacity: [1, 1, 0, 0] }}\n                    transition={\n                      reduce\n                        ? undefined\n                        : { duration: 1, repeat: Number.POSITIVE_INFINITY, ease: \"linear\" }\n                    }\n                    className={cn(\n                      \"pointer-events-none absolute top-1/2 h-6 w-px -translate-y-1/2 bg-foreground\",\n                      char ? \"right-3\" : \"left-1/2 -translate-x-1/2\",\n                    )}\n                  />\n                ) : null}\n\n                {/* Digits roll vertically. Each is absolutely centered so enter and\n                    exit overlap in place — no in-flow reflow, no sideways drift. */}\n                <AnimatePresence initial={false}>\n                  {char ? (\n                    <motion.span\n                      key={char}\n                      initial={\n                        reduce\n                          ? { opacity: 0 }\n                          : { y: 14, opacity: 0, filter: \"blur(4px)\" }\n                      }\n                      animate={\n                        reduce\n                          ? { opacity: 1 }\n                          : { y: 0, opacity: 1, filter: \"blur(0px)\" }\n                      }\n                      exit={\n                        reduce\n                          ? { opacity: 0 }\n                          : { y: -14, opacity: 0, filter: \"blur(4px)\" }\n                      }\n                      transition={\n                        reduce\n                          ? { duration: 0 }\n                          : { duration: 0.22, ease: EASE_OUT }\n                      }\n                      className=\"absolute inset-0 grid place-items-center leading-none\"\n                    >\n                      {mask ? \"•\" : char}\n                    </motion.span>\n                  ) : null}\n                </AnimatePresence>\n              </div>\n            );\n          })}\n        </div>\n\n        <AnimatePresence>\n          {showSuccess ? (\n            <motion.span\n              initial={reduce ? { opacity: 0 } : { scale: 0.6, opacity: 0 }}\n              animate={reduce ? { opacity: 1 } : { scale: 1, opacity: 1 }}\n              exit={reduce ? { opacity: 0 } : { scale: 0.6, opacity: 0 }}\n              transition={\n                reduce\n                  ? { duration: 0 }\n                  : { type: \"spring\", stiffness: 500, damping: 28 }\n              }\n              className=\"pointer-events-none absolute -right-7 top-1/2 -translate-y-1/2 text-emerald-500\"\n              aria-hidden\n            >\n              <svg\n                width=\"20\"\n                height=\"20\"\n                viewBox=\"0 0 24 24\"\n                fill=\"none\"\n                stroke=\"currentColor\"\n                strokeWidth={3}\n                strokeLinecap=\"round\"\n                strokeLinejoin=\"round\"\n              >\n                <title>Verified</title>\n                <motion.path\n                  d=\"M5 13l4 4L19 7\"\n                  initial={reduce ? { pathLength: 1 } : { pathLength: 0 }}\n                  animate={{ pathLength: 1 }}\n                  transition={\n                    reduce\n                      ? { duration: 0 }\n                      : { duration: 0.35, ease: EASE_OUT, delay: 0.1 }\n                  }\n                />\n              </svg>\n            </motion.span>\n          ) : null}\n        </AnimatePresence>\n      </fieldset>\n\n      {message ? (\n        <p\n          aria-live=\"polite\"\n          className={cn(\n            \"text-sm\",\n            showSuccess\n              ? \"text-emerald-500\"\n              : status === \"error\"\n                ? \"text-destructive\"\n                : \"text-muted-foreground\",\n          )}\n        >\n          {message}\n        </p>\n      ) : null}\n    </div>\n  );\n}\n\nfunction sanitize(raw: string, length: number) {\n  return raw.replace(/\\D/g, \"\").slice(0, length);\n}\n\nfunction toSlots(raw: string, length: number) {\n  const digits = sanitize(raw, length);\n  return Array.from({ length }, (_, i) => digits[i] ?? \"\");\n}\n"},{"path":"components/motion/password-input.tsx","type":"registry:component","target":"@components/motion/password-input.tsx","content":"\"use client\";\n\nimport { Lock } from \"lucide-react\";\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\";\nimport {\n  forwardRef,\n  type KeyboardEvent,\n  type ReactNode,\n  useEffect,\n  useId,\n  useMemo,\n  useRef,\n  useState,\n} from \"react\";\nimport { Input, type InputProps } from \"@/components/motion/input\";\nimport { EASE_OUT, SPRING_LAYOUT, SPRING_SWAP } from \"@/lib/ease\";\nimport {\n  MIN_PASSWORD_LENGTH,\n  type PasswordStrength,\n  passwordStrength,\n  STRENGTH_COLORS,\n  STRENGTH_LABELS,\n} from \"@/lib/password-strength\";\nimport { cn } from \"@/lib/utils\";\n\nexport type { PasswordStrength } from \"@/lib/password-strength\";\n\nexport type PasswordRequirement = {\n  id: string;\n  label: string;\n  test: (password: string) => boolean;\n};\n\n/** Default checklist: length, number, symbol, mixed case. */\nexport function defaultPasswordRequirements(\n  minLength: number = MIN_PASSWORD_LENGTH,\n): PasswordRequirement[] {\n  return [\n    {\n      id: \"length\",\n      label: `At least ${minLength} characters`,\n      test: (password) => password.length >= minLength,\n    },\n    { id: \"number\", label: \"A number\", test: (password) => /\\d/.test(password) },\n    {\n      id: \"symbol\",\n      label: \"A symbol\",\n      test: (password) => /[^A-Za-z0-9\\s]/.test(password),\n    },\n    {\n      id: \"case\",\n      label: \"Upper and lowercase letters\",\n      test: (password) => /[a-z]/.test(password) && /[A-Z]/.test(password),\n    },\n  ];\n}\n\nconst STRENGTH_TEXT = [\n  \"text-destructive\",\n  \"text-destructive\",\n  \"text-amber-600 dark:text-amber-400\",\n  \"text-amber-600 dark:text-amber-400\",\n  \"text-(--color-success)\",\n] as const;\n\nexport interface PasswordInputProps\n  extends Omit<InputProps, \"type\" | \"leftIcon\" | \"rightIcon\" | \"success\"> {\n  /** Show the segmented strength meter once something is typed. Default true. */\n  strengthMeter?: boolean;\n  /**\n   * Requirement checklist under the field. `true` uses\n   * `defaultPasswordRequirements(minLength)`; pass an array for custom rules.\n   */\n  requirements?: boolean | PasswordRequirement[];\n  /** Length below which the score is 0 (\"Too short\"). Default 8. */\n  minLength?: number;\n  /** Fires whenever the 0-4 score changes. */\n  onStrengthChange?: (strength: PasswordStrength, label: string) => void;\n  /** Allow revealing the password. Default true. */\n  revealable?: boolean;\n  /** Left icon. Defaults to a lock; pass `null` for none. */\n  icon?: ReactNode;\n}\n\n/**\n * Password field over `Input`: eye/eye-off morphing reveal toggle, a caps-lock\n * warning, a spring-filled strength meter driven by the shared\n * `passwordStrength` heuristic, and an optional requirement checklist.\n */\nexport const PasswordInput = forwardRef<HTMLInputElement, PasswordInputProps>(\n  function PasswordInput(\n    {\n      value: valueProp,\n      defaultValue,\n      onChange,\n      onKeyDown,\n      onKeyUp,\n      onBlur,\n      strengthMeter = true,\n      requirements = false,\n      minLength = MIN_PASSWORD_LENGTH,\n      onStrengthChange,\n      revealable = true,\n      icon,\n      disabled,\n      autoComplete,\n      placeholder = \"Enter a password\",\n      className,\n      ...rest\n    },\n    ref,\n  ) {\n    const reduce = useReducedMotion();\n    const baseId = useId();\n    const statusId = `${baseId}-status`;\n\n    const controlled = valueProp !== undefined;\n    const [internal, setInternal] = useState(defaultValue ?? \"\");\n    const value = controlled ? valueProp : internal;\n\n    const [revealed, setRevealed] = useState(false);\n    const [capsLock, setCapsLock] = useState(false);\n\n    const strength = passwordStrength(value, minLength);\n    const reported = useRef<number | null>(null);\n    const strengthCallback = useRef(onStrengthChange);\n    strengthCallback.current = onStrengthChange;\n    useEffect(() => {\n      if (reported.current === strength) return;\n      reported.current = strength;\n      strengthCallback.current?.(strength, STRENGTH_LABELS[strength]);\n    }, [strength]);\n\n    const rules = useMemo(\n      () =>\n        requirements === true\n          ? defaultPasswordRequirements(minLength)\n          : requirements || [],\n      [requirements, minLength],\n    );\n\n    const readCaps = (event: KeyboardEvent<HTMLInputElement>) => {\n      // getModifierState is missing on some synthetic/IME events.\n      if (typeof event.getModifierState !== \"function\") return;\n      setCapsLock(event.getModifierState(\"CapsLock\"));\n    };\n\n    const showMeter = strengthMeter && value.length > 0;\n\n    return (\n      <div className={cn(\"flex w-full flex-col gap-2\", className)}>\n        <Input\n          {...rest}\n          ref={ref}\n          type={revealed ? \"text\" : \"password\"}\n          value={value}\n          disabled={disabled}\n          placeholder={placeholder}\n          autoComplete={\n            autoComplete ?? (strengthMeter ? \"new-password\" : \"current-password\")\n          }\n          spellCheck={false}\n          aria-describedby={showMeter || capsLock ? statusId : undefined}\n          leftIcon={icon === undefined ? <Lock /> : icon}\n          onChange={(next) => {\n            if (!controlled) setInternal(next);\n            onChange?.(next);\n          }}\n          onKeyDown={(event) => {\n            readCaps(event);\n            onKeyDown?.(event);\n          }}\n          onKeyUp={(event) => {\n            readCaps(event);\n            onKeyUp?.(event);\n          }}\n          onBlur={(event) => {\n            setCapsLock(false);\n            onBlur?.(event);\n          }}\n          rightIcon={\n            revealable ? (\n              <button\n                type=\"button\"\n                disabled={disabled}\n                onClick={() => setRevealed((prev) => !prev)}\n                aria-label={revealed ? \"Hide password\" : \"Show password\"}\n                aria-pressed={revealed}\n                className=\"text-muted-foreground outline-none transition-colors hover:text-foreground focus-visible:text-foreground\"\n              >\n                <EyeMorph closed={revealed} reduce={Boolean(reduce)} />\n              </button>\n            ) : undefined\n          }\n        />\n\n        <div id={statusId} className=\"flex flex-col gap-2 px-1\">\n          <AnimatePresence initial={false}>\n            {capsLock ? (\n              <motion.p\n                key=\"caps\"\n                role=\"status\"\n                initial={reduce ? { opacity: 0 } : { opacity: 0, y: -4 }}\n                animate={{ opacity: 1, y: 0 }}\n                exit={\n                  reduce\n                    ? { opacity: 0, transition: { duration: 0.1 } }\n                    : { opacity: 0, y: -4, transition: { duration: 0.12 } }\n                }\n                transition={{ duration: 0.18, ease: EASE_OUT }}\n                className=\"flex items-center gap-1.5 text-xs text-amber-600 dark:text-amber-400\"\n              >\n                <CapsIcon />\n                Caps Lock is on\n              </motion.p>\n            ) : null}\n          </AnimatePresence>\n\n          <AnimatePresence initial={false}>\n            {showMeter ? (\n              <motion.div\n                key=\"meter\"\n                initial={reduce ? { opacity: 0 } : { opacity: 0, y: -4 }}\n                animate={{ opacity: 1, y: 0 }}\n                exit={\n                  reduce\n                    ? { opacity: 0, transition: { duration: 0.1 } }\n                    : { opacity: 0, y: -4, transition: { duration: 0.12 } }\n                }\n                transition={{ duration: 0.18, ease: EASE_OUT }}\n                className=\"flex items-center gap-3\"\n              >\n                <div className=\"flex flex-1 gap-1.5\" aria-hidden>\n                  {[0, 1, 2, 3].map((index) => (\n                    <span\n                      key={index}\n                      className=\"h-1.5 flex-1 overflow-hidden rounded-full bg-muted-foreground/20\"\n                    >\n                      {/* scaleX, not width — transform only, off layout. */}\n                      <motion.span\n                        initial={false}\n                        animate={{ scaleX: index < strength ? 1 : 0 }}\n                        transition={\n                          reduce\n                            ? { duration: 0 }\n                            : { ...SPRING_LAYOUT, delay: index * 0.03 }\n                        }\n                        className={cn(\n                          \"block h-full w-full origin-left rounded-full transition-colors duration-300\",\n                          STRENGTH_COLORS[strength],\n                        )}\n                      />\n                    </span>\n                  ))}\n                </div>\n                <span\n                  aria-live=\"polite\"\n                  className=\"relative grid min-w-16 justify-items-end overflow-hidden text-xs font-medium\"\n                >\n                  <span className=\"sr-only\">Password strength: </span>\n                  <AnimatePresence initial={false} mode=\"popLayout\">\n                    <motion.span\n                      key={strength}\n                      initial={reduce ? { opacity: 0 } : { opacity: 0, y: 8 }}\n                      animate={{ opacity: 1, y: 0 }}\n                      exit={\n                        reduce\n                          ? { opacity: 0, transition: { duration: 0.08 } }\n                          : { opacity: 0, y: -8, transition: { duration: 0.1 } }\n                      }\n                      transition={reduce ? { duration: 0.12 } : SPRING_SWAP}\n                      className={STRENGTH_TEXT[strength]}\n                    >\n                      {STRENGTH_LABELS[strength]}\n                    </motion.span>\n                  </AnimatePresence>\n                </span>\n              </motion.div>\n            ) : null}\n          </AnimatePresence>\n\n          {rules.length > 0 ? (\n            <ul className=\"grid gap-1.5 sm:grid-cols-2\" aria-label=\"Password requirements\">\n              {rules.map((rule) => {\n                const met = rule.test(value);\n                return (\n                  <li\n                    key={rule.id}\n                    className={cn(\n                      \"flex items-center gap-2 text-xs transition-colors duration-200\",\n                      met ? \"text-foreground\" : \"text-muted-foreground\",\n                    )}\n                  >\n                    <RequirementMark met={met} reduce={Boolean(reduce)} />\n                    <span>{rule.label}</span>\n                    <span className=\"sr-only\">{met ? \"(met)\" : \"(not met)\"}</span>\n                  </li>\n                );\n              })}\n            </ul>\n          ) : null}\n        </div>\n      </div>\n    );\n  },\n);\n\n/** Eye that morphs to eye-off: a slash draws across and the pupil dims. */\nfunction EyeMorph({ closed, reduce }: { closed: boolean; reduce: boolean }) {\n  const transition = reduce ? { duration: 0 } : { duration: 0.22, ease: EASE_OUT };\n  return (\n    <svg\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth={2}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      aria-hidden=\"true\"\n    >\n      <path d=\"M2.06 12.35a1 1 0 0 1 0-.7 10.75 10.75 0 0 1 19.88 0 1 1 0 0 1 0 .7 10.75 10.75 0 0 1-19.88 0\" />\n      <motion.circle\n        cx={12}\n        cy={12}\n        r={3}\n        initial={false}\n        animate={{ opacity: closed ? 0.35 : 1 }}\n        transition={transition}\n      />\n      <motion.path\n        d=\"M3 3l18 18\"\n        initial={false}\n        animate={{ pathLength: closed ? 1 : 0, opacity: closed ? 1 : 0 }}\n        transition={transition}\n      />\n    </svg>\n  );\n}\n\nfunction RequirementMark({ met, reduce }: { met: boolean; reduce: boolean }) {\n  return (\n    <span\n      aria-hidden=\"true\"\n      className={cn(\n        \"grid size-4 shrink-0 place-items-center rounded-full border transition-colors duration-200\",\n        met\n          ? \"border-(--color-success) bg-(--color-success) text-background\"\n          : \"border-muted-foreground/40 bg-transparent text-transparent\",\n      )}\n    >\n      <svg viewBox=\"0 0 24 24\" fill=\"none\" className=\"size-3\" aria-hidden=\"true\">\n        <motion.path\n          d=\"M5 12.5l4.5 4.5L19 7.5\"\n          stroke=\"currentColor\"\n          strokeWidth={3}\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          initial={false}\n          animate={{ pathLength: met ? 1 : 0 }}\n          transition={reduce ? { duration: 0 } : { duration: 0.25, ease: EASE_OUT }}\n        />\n      </svg>\n    </span>\n  );\n}\n\nfunction CapsIcon() {\n  return (\n    <svg\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth={2}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      className=\"size-3.5\"\n      aria-hidden=\"true\"\n    >\n      <path d=\"M12 4 4 12h4v4h8v-4h4z\" />\n      <path d=\"M8 20h8\" />\n    </svg>\n  );\n}\n"},{"path":"lib/ease.ts","type":"registry:lib","target":"@lib/ease.ts","content":"// Shared motion tokens. Easing curves mirror the CSS custom properties in\n// globals.css; springs are the canonical physics used across components.\n// Strong custom variants — defaults like `ease-in`/`ease-out` feel weak.\n\nexport const EASE_OUT = [0.16, 1, 0.3, 1] as const;\nexport const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;\nexport const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;\n\n/** CSS string form of EASE_OUT for inline style transitions. */\nexport const EASE_OUT_CSS = \"cubic-bezier(0.16, 1, 0.3, 1)\";\n\n/** Press feedback on buttons and other tappable surfaces. */\nexport const SPRING_PRESS = {\n  type: \"spring\",\n  stiffness: 500,\n  damping: 30,\n  mass: 0.6,\n} as const;\n\n/** Content swaps — label/icon slots trading places inside a control. */\nexport const SPRING_SWAP = {\n  type: \"spring\",\n  stiffness: 460,\n  damping: 30,\n  mass: 0.55,\n} as const;\n\n/** Overlay panel entrances — modals and sheets summoned by pointer. */\nexport const SPRING_PANEL = {\n  type: \"spring\",\n  stiffness: 420,\n  damping: 40,\n  mass: 0.5,\n} as const;\n\n/** Shared-layout glides — pills, indicators and panels morphing between positions. */\nexport const SPRING_LAYOUT = {\n  type: \"spring\",\n  stiffness: 360,\n  damping: 32,\n  mass: 0.6,\n} as const;\n\n/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt, dock). */\nexport const SPRING_MOUSE = {\n  stiffness: 200,\n  damping: 15,\n  mass: 0.3,\n} as const;\n\n/** Dragged handles and fills (sliders) — critically damped `useSpring` config,\n * so the value follows the pointer butterily and never rebounds off an end. */\nexport const SPRING_GLIDE = {\n  stiffness: 700,\n  damping: 50,\n  mass: 0.5,\n} as const;\n"},{"path":"lib/password-strength.ts","type":"registry:lib","target":"@lib/password-strength.ts","content":"// Shared password strength heuristic, used by SignUpForm and PasswordInput.\n\nexport const MIN_PASSWORD_LENGTH = 8;\n\n/** Label per score, index 0-4. */\nexport const STRENGTH_LABELS = [\n  \"Too short\",\n  \"Weak\",\n  \"Fair\",\n  \"Good\",\n  \"Strong\",\n] as const;\n\n/** Fill color per score, index 0-4 (Tailwind background classes). */\nexport const STRENGTH_COLORS = [\n  \"bg-destructive\",\n  \"bg-destructive\",\n  \"bg-amber-500\",\n  \"bg-amber-400\",\n  \"bg-(--color-success)\",\n] as const;\n\nexport type PasswordStrength = 0 | 1 | 2 | 3 | 4;\n\n/**\n * Length-weighted strength score, 0-4. NIST SP 800-63B advises against\n * composition requirements and treats length as the dominant factor, so extra\n * character classes only nudge the score — they can't rescue a short password.\n * This is a heuristic for feedback, not entropy estimation; pair it with a\n * breach-list check server-side for anything real.\n */\nexport function passwordStrength(\n  password: string,\n  minLength: number = MIN_PASSWORD_LENGTH,\n): PasswordStrength {\n  if (password.length < minLength) return 0;\n\n  let score = 1;\n  if (password.length >= 12) score += 1;\n  if (password.length >= 16) score += 1;\n\n  const classes = [/[a-z]/, /[A-Z]/, /\\d/, /[^A-Za-z0-9]/].filter((pattern) =>\n    pattern.test(password),\n  ).length;\n  if (classes >= 3) score += 1;\n\n  return Math.min(score, 4) as PasswordStrength;\n}\n"},{"path":"lib/utils.ts","type":"registry:lib","target":"@lib/utils.ts","content":"import { clsx, type ClassValue } from \"clsx\"\nimport { twMerge } from \"tailwind-merge\"\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs))\n}\n"},{"path":"components/motion/button/base.tsx","type":"registry:component","target":"@components/motion/button/base.tsx","content":"\"use client\";\n\nimport {\n  AnimatePresence,\n  type HTMLMotionProps,\n  motion,\n  useReducedMotion,\n} from \"motion/react\";\nimport {\n  forwardRef,\n  type PointerEvent,\n  type ReactNode,\n  useCallback,\n  useRef,\n  useState,\n} from \"react\";\nimport { EASE_OUT, SPRING_PRESS } from \"@/lib/ease\";\nimport { useHoverCapable } from \"@/lib/hooks/use-hover-capable\";\nimport { TOUCH_TARGET_CLASS } from \"@/lib/touch\";\nimport { cn } from \"@/lib/utils\";\n\nexport type ButtonVariant = \"primary\" | \"secondary\" | \"ghost\" | \"outline\";\nexport type ButtonSize = \"sm\" | \"md\" | \"lg\" | \"icon\";\n\nexport interface ButtonProps extends Omit<\n  HTMLMotionProps<\"button\">,\n  \"children\"\n> {\n  variant?: ButtonVariant;\n  size?: ButtonSize;\n  pressScale?: number;\n  /** Spawn a Material-style ripple from the press point. Off by default. */\n  ripple?: boolean;\n  children?: ReactNode;\n}\n\nexport interface ButtonLinkProps extends Omit<\n  HTMLMotionProps<\"a\">,\n  \"children\"\n> {\n  variant?: ButtonVariant;\n  size?: ButtonSize;\n  pressScale?: number;\n  children?: ReactNode;\n}\n\ntype Ripple = { id: number; x: number; y: number; size: number };\n\nconst VARIANT_CLASS: Record<ButtonVariant, string> = {\n  primary: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n  secondary: \"border border-border bg-card text-foreground hover:border-border\",\n  ghost: \"text-muted-foreground hover:text-foreground hover:bg-primary/5\",\n  outline:\n    \"border border-border bg-transparent text-foreground hover:bg-primary/5\",\n};\n\nconst SIZE_CLASS: Record<ButtonSize, string> = {\n  sm: \"h-8 px-3 text-xs gap-1.5 rounded-full\",\n  md: \"h-10 px-5 text-sm gap-2 rounded-full\",\n  lg: \"h-12 px-6 text-base gap-2 rounded-full\",\n  icon: \"h-8 w-8 rounded-lg\",\n};\n\nexport const Button = forwardRef<HTMLButtonElement, ButtonProps>(\n  function Button(\n    {\n      variant = \"primary\",\n      size = \"md\",\n      pressScale = 0.93,\n      ripple = false,\n      className,\n      children,\n      onPointerDown,\n      ...rest\n    },\n    ref,\n  ) {\n    const reduce = useReducedMotion();\n    const canHover = useHoverCapable();\n    const [ripples, setRipples] = useState<Ripple[]>([]);\n    const nextId = useRef(0);\n\n    const handlePointerDown = useCallback(\n      (event: PointerEvent<HTMLButtonElement>) => {\n        if (ripple && !reduce) {\n          const rect = event.currentTarget.getBoundingClientRect();\n          const size = Math.max(rect.width, rect.height) * 2;\n          const id = nextId.current++;\n          setRipples((prev) => [\n            ...prev,\n            {\n              id,\n              x: event.clientX - rect.left,\n              y: event.clientY - rect.top,\n              size,\n            },\n          ]);\n        }\n        onPointerDown?.(event);\n      },\n      [ripple, reduce, onPointerDown],\n    );\n\n    return (\n      <motion.button\n        ref={ref}\n        type=\"button\"\n        whileTap={reduce ? undefined : { scale: pressScale }}\n        whileHover={reduce || !canHover ? undefined : { scale: 1.02 }}\n        transition={SPRING_PRESS}\n        onPointerDown={handlePointerDown}\n        className={cn(\n          \"relative inline-flex items-center justify-center font-medium select-none\",\n          \"transition-colors\",\n          \"disabled:pointer-events-none disabled:opacity-50\",\n          // The ripple layer clips itself, so on a touch screen the button can\n          // stop clipping and let its enlarged hit area overhang.\n          ripple && \"overflow-hidden pointer-coarse:overflow-visible\",\n          TOUCH_TARGET_CLASS,\n          VARIANT_CLASS[variant],\n          SIZE_CLASS[size],\n          className,\n        )}\n        {...rest}\n      >\n        {ripple && !reduce ? (\n          <span className=\"pointer-events-none absolute inset-0 overflow-hidden rounded-[inherit]\">\n            <AnimatePresence>\n              {ripples.map((r) => (\n                <motion.span\n                  key={r.id}\n                  className=\"absolute rounded-full bg-current\"\n                  style={{\n                    left: r.x,\n                    top: r.y,\n                    width: r.size,\n                    height: r.size,\n                    x: \"-50%\",\n                    y: \"-50%\",\n                  }}\n                  initial={{ scale: 0.05, opacity: 0.3 }}\n                  animate={{ scale: 1, opacity: 0 }}\n                  exit={{ opacity: 0 }}\n                  transition={{ duration: 1.6, ease: EASE_OUT }}\n                  onAnimationComplete={() =>\n                    setRipples((prev) => prev.filter((x) => x.id !== r.id))\n                  }\n                />\n              ))}\n            </AnimatePresence>\n          </span>\n        ) : null}\n        {children}\n      </motion.button>\n    );\n  },\n);\n\nexport const ButtonLink = forwardRef<HTMLAnchorElement, ButtonLinkProps>(\n  function ButtonLink(\n    {\n      variant = \"primary\",\n      size = \"md\",\n      pressScale = 0.93,\n      className,\n      children,\n      ...rest\n    },\n    ref,\n  ) {\n    const reduce = useReducedMotion();\n    const canHover = useHoverCapable();\n\n    return (\n      <motion.a\n        ref={ref}\n        whileTap={reduce ? undefined : { scale: pressScale }}\n        whileHover={reduce || !canHover ? undefined : { scale: 1.02 }}\n        transition={SPRING_PRESS}\n        className={cn(\n          \"relative inline-flex items-center justify-center font-medium select-none\",\n          \"transition-colors\",\n          TOUCH_TARGET_CLASS,\n          VARIANT_CLASS[variant],\n          SIZE_CLASS[size],\n          className,\n        )}\n        {...rest}\n      >\n        {children}\n      </motion.a>\n    );\n  },\n);\n"},{"path":"components/motion/button/magnetic.tsx","type":"registry:component","target":"@components/motion/button/magnetic.tsx","content":"\"use client\";\n\nimport { forwardRef } from \"react\";\nimport { Magnetic } from \"../magnetic\";\nimport { Button, type ButtonProps } from \"./base\";\n\nexport interface MagneticButtonProps extends ButtonProps {\n  /** Magnetic pull strength. Default 0.25. */\n  strength?: number;\n  /** Class applied to the magnetic wrapper. */\n  magneticClassName?: string;\n}\n\nexport const MagneticButton = forwardRef<HTMLButtonElement, MagneticButtonProps>(function MagneticButton(\n  { strength = 0.25, magneticClassName, children, ...rest },\n  ref,\n) {\n  return (\n    <Magnetic strength={strength} className={magneticClassName}>\n      <Button ref={ref} {...rest}>\n        {children}\n      </Button>\n    </Magnetic>\n  );\n});\n"},{"path":"components/motion/button/stateful.tsx","type":"registry:component","target":"@components/motion/button/stateful.tsx","content":"\"use client\";\n\nimport { Check, Loader2, X } from \"lucide-react\";\nimport {\n  AnimatePresence,\n  motion,\n  useReducedMotion,\n  type Variants,\n} from \"motion/react\";\nimport {\n  forwardRef,\n  type ReactNode,\n  useLayoutEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport { EASE_OUT, SPRING_SWAP } from \"@/lib/ease\";\nimport { Button, type ButtonProps } from \"./base\";\n\nexport type ButtonState = \"idle\" | \"loading\" | \"success\" | \"error\";\n\nexport interface StatefulButtonProps extends Omit<ButtonProps, \"children\"> {\n  state?: ButtonState;\n  children: ReactNode;\n  loadingText?: ReactNode;\n  successText?: ReactNode;\n  errorText?: ReactNode;\n  icon?: ReactNode;\n}\n\nconst CASCADE_STAGGER = 0.025;\nconst ROLL_BLUR = \"blur(6px)\";\n\nconst CASCADE_LETTER_VARIANTS: Variants = {\n  initial: { opacity: 0, y: \"105%\", filter: ROLL_BLUR },\n  animate: (delay: number = 0) => ({\n    opacity: 1,\n    y: \"0%\",\n    filter: \"blur(0px)\",\n    transition: { ...SPRING_SWAP, delay },\n  }),\n  exit: (delay: number = 0) => ({\n    opacity: 0,\n    y: \"-105%\",\n    filter: ROLL_BLUR,\n    transition: { duration: 0.16, ease: EASE_OUT, delay: delay * 0.5 },\n  }),\n};\n\nconst ICON_VARIANTS: Variants = {\n  // Width collapses too, so the icon adds/removes its own space smoothly\n  // instead of popping the row width in a single frame.\n  initial: { opacity: 0, width: 0, scale: 0.7, filter: ROLL_BLUR },\n  animate: {\n    opacity: 1,\n    width: \"1.5rem\",\n    scale: 1,\n    filter: \"blur(0px)\",\n    transition: SPRING_SWAP,\n  },\n  exit: {\n    opacity: 0,\n    width: 0,\n    scale: 0.7,\n    filter: ROLL_BLUR,\n    transition: { duration: 0.16, ease: EASE_OUT },\n  },\n};\n\nfunction IconSlot({ keyId, children }: { keyId: string; children: ReactNode }) {\n  const reduce = useReducedMotion();\n  return (\n    <motion.span\n      key={keyId}\n      variants={ICON_VARIANTS}\n      initial={reduce ? { opacity: 0 } : \"initial\"}\n      animate={reduce ? { opacity: 1 } : \"animate\"}\n      exit={reduce ? { opacity: 0 } : \"exit\"}\n      transition={reduce ? { duration: 0.15 } : undefined}\n      className=\"inline-grid shrink-0 place-items-center overflow-hidden\"\n    >\n      {children}\n    </motion.span>\n  );\n}\n\nfunction TextSlot({\n  value,\n  children,\n}: {\n  value: string;\n  children: ReactNode;\n}) {\n  const reduce = useReducedMotion();\n  const measureRef = useRef<HTMLSpanElement>(null);\n  const [width, setWidth] = useState<number>();\n  const label = typeof children === \"string\" ? children : null;\n  const cascade = label !== null && !reduce;\n\n  // Measure strings with the same per-letter layout as the cascade. Measuring\n  // the whole string preserves kerning, which can make it narrower than the\n  // inline-block letters and clip the final glyph during the width animation.\n  useLayoutEffect(() => {\n    const nextWidth = measureRef.current?.offsetWidth;\n    if (!nextWidth) return;\n    setWidth((current) => (current === nextWidth ? current : nextWidth));\n  });\n\n  return (\n    <motion.span\n      initial={false}\n      animate={{ width }}\n      transition={reduce ? { duration: 0 } : SPRING_SWAP}\n      className=\"relative inline-block overflow-hidden whitespace-nowrap align-bottom\"\n    >\n      <span\n        ref={measureRef}\n        aria-hidden\n        className=\"invisible inline-block whitespace-nowrap\"\n      >\n        {cascade\n          ? label.split(\"\").map((char, index) => (\n              <span\n                // biome-ignore lint/suspicious/noArrayIndexKey: position is the slot identity.\n                key={index}\n                className=\"inline-block whitespace-pre\"\n              >\n                {char}\n              </span>\n            ))\n          : children}\n      </span>\n\n      {cascade ? (\n        <>\n          <span className=\"sr-only\">{label}</span>\n          <AnimatePresence initial={false}>\n            <motion.span\n              key={`cascade-${value}`}\n              aria-hidden\n              initial=\"initial\"\n              animate=\"animate\"\n              exit=\"exit\"\n              className=\"absolute left-0 top-0 inline-block whitespace-pre\"\n            >\n              {label.split(\"\").map((char, index) => (\n                <motion.span\n                  // biome-ignore lint/suspicious/noArrayIndexKey: position is the slot identity.\n                  key={index}\n                  custom={index * CASCADE_STAGGER}\n                  variants={CASCADE_LETTER_VARIANTS}\n                  className=\"inline-block whitespace-pre will-change-[opacity,filter,transform]\"\n                >\n                  {char}\n                </motion.span>\n              ))}\n            </motion.span>\n          </AnimatePresence>\n        </>\n      ) : (\n        <AnimatePresence initial={false}>\n          <motion.span\n            key={`text-${value}`}\n            initial={reduce ? { opacity: 0 } : { opacity: 0, y: 14, filter: ROLL_BLUR }}\n            animate={reduce ? { opacity: 1 } : { opacity: 1, y: 0, filter: \"blur(0px)\" }}\n            exit={reduce ? { opacity: 0 } : { opacity: 0, y: -14, filter: ROLL_BLUR }}\n            transition={reduce ? { duration: 0.15 } : SPRING_SWAP}\n            className=\"absolute left-0 top-0 inline-block will-change-[opacity,filter,transform]\"\n          >\n            {children}\n          </motion.span>\n        </AnimatePresence>\n      )}\n    </motion.span>\n  );\n}\n\nexport const StatefulButton = forwardRef<HTMLButtonElement, StatefulButtonProps>(function StatefulButton(\n  {\n    state = \"idle\",\n    children,\n    loadingText = \"Loading\",\n    successText = \"Done\",\n    errorText = \"Try again\",\n    icon,\n    disabled,\n    ...rest\n  },\n  ref,\n) {\n  const isBusy = state === \"loading\";\n  const stateText =\n    state === \"loading\"\n      ? loadingText\n      : state === \"success\"\n        ? successText\n        : state === \"error\"\n        ? errorText\n        : children;\n  const textKey =\n    typeof stateText === \"string\" ? `${state}-${stateText}` : state;\n\n  return (\n    <Button ref={ref} disabled={disabled || isBusy} aria-busy={isBusy} whileHover={undefined} {...rest}>\n      <span\n        aria-live=\"polite\"\n        className=\"relative inline-flex items-center justify-center overflow-hidden\"\n      >\n        <AnimatePresence initial={false}>\n          {state === \"loading\" ? (\n            <IconSlot keyId=\"loading-icon\">\n              <Loader2 className=\"h-4 w-4 animate-spin\" />\n            </IconSlot>\n          ) : null}\n          {state === \"success\" ? (\n            <IconSlot keyId=\"success-icon\">\n              <Check className=\"h-4 w-4\" />\n            </IconSlot>\n          ) : null}\n          {state === \"error\" ? (\n            <IconSlot keyId=\"error-icon\">\n              <X className=\"h-4 w-4\" />\n            </IconSlot>\n          ) : null}\n        </AnimatePresence>\n\n        <TextSlot value={textKey}>{stateText}</TextSlot>\n\n        <AnimatePresence initial={false}>\n          {state === \"idle\" && icon ? (\n            <IconSlot keyId=\"idle-icon\">{icon}</IconSlot>\n          ) : null}\n        </AnimatePresence>\n      </span>\n    </Button>\n  );\n});\n"},{"path":"lib/touch.ts","type":"registry:lib","target":"@lib/touch.ts","content":"// Shared touch primitives. iOS and iPadOS run their own gestures on top of the\n// page — the long-press selection callout and the selection it drags in with\n// it — and they win: once the platform claims a touch it cancels ours\n// mid-gesture, so a press-and-hold or a drag simply dies. Surfaces that own\n// their gesture have to opt out.\n//\n// What the two classes below cover, precisely:\n// - `-webkit-touch-callout: none` stops iOS's long-press callout. WebKit-only:\n//   it is not a property other engines have, so it is inert everywhere else.\n// - `user-select: none` stops the long-press selection on every engine,\n//   Android included, and stops a drag from painting a selection under the\n//   cursor. It is inherited, so it reaches every descendant — which is why the\n//   two classes differ only in whether they apply it unconditionally.\n// What neither covers:\n// - Chrome for Android's long-press menu on a link or an image. No CSS\n//   suppresses it; a gesture surface that wraps one needs its own\n//   `onContextMenu` with `preventDefault()`.\n// - The native drag of an `<img>` or `<a>` descendant. `-webkit-user-drag` is\n//   not inherited and plain divs and buttons are not drag sources, so setting\n//   it on the surface does nothing — the child itself needs `draggable={false}`.\n\n/**\n * Classes for a surface that *is* the control: a thumb, a drum, a stage, a\n * handle, a hold button. Selection is suppressed on every input, because a\n * drag that highlights the control's own label is wrong on a mouse too.\n * Compose with `touch-none` when the surface also owns the scroll axis — leave\n * it off when the page must still scroll from there.\n */\nexport const TOUCH_GESTURE_CLASS = \"select-none [-webkit-touch-callout:none]\";\n\n/**\n * The same opt-out for a gesture surface that wraps content the consumer owns:\n * a scroller, a context-menu trigger, a sheet header, a list row. Selection is\n * suppressed only where the platform runs its own press gestures — a coarse\n * pointer — so a mouse user can still select and copy that content. If the\n * gesture itself would paint a selection under the cursor, add `select-none`\n * for the duration of the gesture rather than reaching for\n * `TOUCH_GESTURE_CLASS`.\n *\n * `pointer: coarse` describes the *primary* pointer and nothing else, so a\n * hybrid machine reads it wrong in both directions: a tablet with a mouse\n * plugged in keeps touch as primary and loses mouse selection, and a laptop\n * with a touchscreen keeps the mouse as primary and leaves selection live\n * under a finger. No media query can answer per interaction — the query is\n * about the device, and the question is about the gesture in progress. The\n * default stays here because it is right on the machines that are one thing or\n * the other, and losing a selection is a nuisance; where the miss costs a\n * *gesture* instead, the surface pairs it with `holdSelection` on the press.\n */\nexport const TOUCH_GESTURE_CONTENT_CLASS =\n  \"[-webkit-touch-callout:none] pointer-coarse:select-none\";\n\n/**\n * Grow a small control's *hit area* to at least 44×44px (WCAG 2.5.5, Apple\n * HIG) on a coarse pointer, without changing what it looks like or the space\n * it takes in layout. An invisible `::after` box, centered on the control and\n * `max(100%, 2.75rem)` on each axis, catches the taps: a 32px icon button\n * reaches 6px past each edge, a wide 40px button only grows vertically, and a\n * control already 44px or larger is left as it is. Hit-testing a pseudo\n * element lands on its owner, so handlers, focus and `:active` all behave.\n *\n * Mouse and trackpad rendering is untouched — nothing here applies outside\n * `pointer: coarse`. Requirements on the element it goes on:\n * - It must be the containing block: add `relative` (and keep it mergeable, so\n *   a consumer's `absolute` still wins through `cn`).\n * - It must not clip itself (`overflow-hidden`), or the overhang is clipped\n *   away along with its hit area.\n * - It must render children: `<input>` and other void elements have no\n *   `::after`. Put it on the wrapper or label instead.\n * - It must not already use `::after` for something visible.\n *\n * Centered 44px means two 32px buttons with a 4px gap overlap by 8px; the\n * later one wins that strip, which still ends well short of the earlier\n * button's visual center. Below a ~32px pitch the later neighbour starts\n * eating into that center — use `TOUCH_TARGET_Y_CLASS` for such dense rows.\n *\n * The overhang is real geometry: inside a scroll container it counts as\n * scrollable overflow, so a control flush against a scroller's edge can add a\n * few pixels of scroll on touch screens. Give such a scroller a little padding\n * on that side.\n */\nexport const TOUCH_TARGET_CLASS =\n  \"pointer-coarse:after:absolute pointer-coarse:after:top-1/2 pointer-coarse:after:left-1/2 pointer-coarse:after:size-[max(100%,2.75rem)] pointer-coarse:after:-translate-x-1/2 pointer-coarse:after:-translate-y-1/2 pointer-coarse:after:content-['']\";\n\n/**\n * `TOUCH_TARGET_CLASS` on the vertical axis only, for controls packed tighter\n * than ~32px apart in a row (pagination dots, dense swatches), where growing\n * sideways would hand most of each control to its neighbour. Same rules.\n */\nexport const TOUCH_TARGET_Y_CLASS =\n  \"pointer-coarse:after:absolute pointer-coarse:after:inset-x-0 pointer-coarse:after:top-1/2 pointer-coarse:after:h-[max(100%,2.75rem)] pointer-coarse:after:-translate-y-1/2 pointer-coarse:after:content-['']\";\n\n/**\n * Suppress selection on `element` for as long as a gesture is running on it,\n * whatever the primary pointer of the machine happens to be. Returns the\n * release. Inline, so it wins over the class above and is gone again the\n * moment the gesture ends.\n *\n * For the press gestures a native selection would otherwise steal — a\n * long-press that opens a menu. Elsewhere prefer the classes: a surface that\n * takes selection away for the whole session is a surface whose text nobody\n * can copy.\n */\nexport function holdSelection(element: HTMLElement) {\n  element.style.setProperty(\"user-select\", \"none\");\n  element.style.setProperty(\"-webkit-user-select\", \"none\");\n  return () => {\n    element.style.removeProperty(\"user-select\");\n    element.style.removeProperty(\"-webkit-user-select\");\n  };\n}\n\n/**\n * Pointer capture, best effort. WebKit throws `NotFoundError` when the pointer\n * is already gone by the time the handler runs — routine on iOS, where the\n * system can claim the touch first — and an uncaught throw takes the rest of\n * the handler, the gesture included, down with it. Touch pointers carry\n * implicit capture anyway, so losing it is never fatal.\n */\nexport function capturePointer(element: Element, pointerId: number) {\n  try {\n    element.setPointerCapture(pointerId);\n  } catch {\n    // Pointer is no longer active — implicit capture still applies on touch.\n  }\n}\n\n/** Release a capture taken with `capturePointer`, ignoring a stale pointer. */\nexport function releasePointer(element: Element, pointerId: number) {\n  try {\n    if (element.hasPointerCapture(pointerId)) {\n      element.releasePointerCapture(pointerId);\n    }\n  } catch {\n    // Capture was already dropped by the browser.\n  }\n}\n\n/**\n * Whether this event came from a pointer that is *hovering*: not a touch, and\n * not currently pressed. Which input the user is holding right now is not\n * something a device capability can answer — a touchscreen laptop hovers and\n * taps, and iPadOS reports a fine hovering pointer for a finger — so both\n * paths stay live and each handler branches on the event it was given.\n *\n * A pen resting on the glass is making contact, not hovering: `buttons` is the\n * tell, and it sends a pen tap down the same route a finger takes.\n *\n * This answers what an *enter* asks. A leave is the other half of a pair and\n * has to be read against the enter that started it — `useHoverGesture` in\n * `lib/hooks/use-hover-gesture` does that, and hover surfaces should use it\n * rather than asking this question twice.\n */\nexport const isHoveringPointer = (event: {\n  pointerType: string;\n  buttons: number;\n}) => event.pointerType !== \"touch\" && event.buttons === 0;\n"},{"path":"lib/hooks/use-hover-capable.ts","type":"registry:hook","target":"@lib/hooks/use-hover-capable.ts","content":"\"use client\";\n\nimport { useEffect, useState } from \"react\";\n\n/**\n * Returns true only on devices that have a true hover (mouse / trackpad).\n * Touch devices fire phantom `:hover` on tap that sticks until tap-elsewhere\n * — gate hover-only effects (scale lifts, magnetic pulls) behind this.\n */\nexport function useHoverCapable() {\n  const [canHover, setCanHover] = useState(false);\n\n  useEffect(() => {\n    if (typeof window === \"undefined\" || !window.matchMedia) return;\n    const mq = window.matchMedia(\"(hover: hover) and (pointer: fine)\");\n    const update = () => setCanHover(mq.matches);\n    update();\n    mq.addEventListener?.(\"change\", update);\n    return () => mq.removeEventListener?.(\"change\", update);\n  }, []);\n\n  return canHover;\n}\n"},{"path":"components/motion/magnetic.tsx","type":"registry:component","target":"@components/motion/magnetic.tsx","content":"\"use client\";\n\nimport { motion, useMotionValue, useReducedMotion, useSpring } from \"motion/react\";\nimport { useRef, type ReactNode } from \"react\";\nimport { SPRING_MOUSE } from \"@/lib/ease\";\nimport { useHoverCapable } from \"@/lib/hooks/use-hover-capable\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface MagneticProps {\n  children: ReactNode;\n  strength?: number;\n  className?: string;\n}\n\nexport function Magnetic({ children, strength = 0.35, className }: MagneticProps) {\n  const ref = useRef<HTMLDivElement>(null);\n  const reduce = useReducedMotion();\n  const canHover = useHoverCapable();\n  // Decorative cursor-follow: skip on touch (phantom hover) and reduced motion.\n  const enabled = !reduce && canHover;\n  const x = useMotionValue(0);\n  const y = useMotionValue(0);\n  const sx = useSpring(x, SPRING_MOUSE);\n  const sy = useSpring(y, SPRING_MOUSE);\n\n  const onMove = (e: React.MouseEvent<HTMLDivElement>) => {\n    const el = ref.current;\n    if (!el || !enabled) return;\n    const rect = el.getBoundingClientRect();\n    x.set((e.clientX - rect.left - rect.width / 2) * strength);\n    y.set((e.clientY - rect.top - rect.height / 2) * strength);\n  };\n\n  const onLeave = () => {\n    x.set(0);\n    y.set(0);\n  };\n\n  return (\n    <motion.div\n      ref={ref}\n      onMouseMove={onMove}\n      onMouseLeave={onLeave}\n      style={{ x: sx, y: sy }}\n      className={cn(\"inline-block\", className)}\n    >\n      {children}\n    </motion.div>\n  );\n}\n"}]}