bigbullui logobigbullui
Componentpickers

Two Factor

2FA setup panel with animated QR scan line, recovery codes and copy action.

Preview

Live Preview

İki adımlı doğrulama

Doğrulama uygulamanla QR kodu tara.

Kurtarma kodları
4F7A-9K2MB3X8-QP1DM2VD-88ZLT9RC-55WNJ6HB-33YFW4KN-77TSP8QX-21GBR5ZM-66JV

Installation

Tokens guide →

Add to your project via CLI (zero dependencies, code you own):

npx bigbullui add two-factor

Or install the full npm package:

npm install bigbullui

Usage

import { TwoFactor } from "@/components/ui/two-factor";

<TwoFactor />

Source code

two-factor.tsx

Zero dependencies (only React + utils). Copy and paste directly into your project:

"use client";

import * as React from "react";
import { cn } from "./lib/utils";

export interface TwoFactorProps extends React.HTMLAttributes<HTMLDivElement> {
  /** kurtarma kodları; boşsa üretilmiş örnek gösterilir */
  recoveryCodes?: string[];
  onConfirm?: () => void;
}

/** 2FA kurulum paneli: QR alanı + kurtarma kodları (indirilebilir hissi) + onay. */
export function TwoFactor({ recoveryCodes, onConfirm, className, ...props }: TwoFactorProps) {
  const codes = recoveryCodes ?? [
    "4F7A-9K2M", "B3X8-QP1D", "M2VD-88ZL", "T9RC-55WN",
    "J6HB-33YF", "W4KN-77TS", "P8QX-21GB", "R5ZM-66JV",
  ];
  const [revealed, setRevealed] = React.useState(false);
  const [copied, setCopied] = React.useState(false);

  const copyAll = () => {
    try { void navigator.clipboard?.writeText(codes.join("\n")); } catch { /* yoksay */ }
    setCopied(true);
    setTimeout(() => setCopied(false), 1800);
  };

  return (
    <div className={cn("w-full max-w-sm space-y-5", className)} {...props}>
      <style>{`
        @keyframes twofaScan {
          0% { top: 4%; opacity: 0.9; }
          50% { top: 92%; opacity: 1; }
          100% { top: 4%; opacity: 0.9; }
        }
        @keyframes twofaIn {
          from { opacity: 0; transform: translateY(8px); }
          to { opacity: 1; transform: translateY(0); }
        }
      `}</style>

      <div className="text-center">
        <h3 className="text-lg font-semibold tracking-tight">İki adımlı doğrulama</h3>
        <p className="mt-1 text-sm text-muted-foreground">Doğrulama uygulamanla QR kodu tara.</p>
      </div>

      {/* QR simülasyonu */}
      <div className="relative mx-auto size-40 overflow-hidden rounded-lg border-2 border-dashed border-border bg-card p-3">
        <div className="grid size-full grid-cols-8 gap-0.5" aria-hidden="true">
          {Array.from({ length: 64 }).map((_, i) => {
            const on = ((i * 7 + 13) % 17) % 3 !== 0;
            return <div key={i} className={cn("rounded-[1px]", on ? "bg-foreground" : "bg-transparent")} style={{ animationDelay: `${i * 12}ms` }} />;
          })}
        </div>
        <div className="absolute left-2 right-2 h-0.5 bg-destructive/80" style={{ animation: "twofaScan 2.4s ease-in-out infinite" }} aria-hidden="true" />
      </div>

      {/* Kurtarma kodları */}
      <div className="rounded-md border border-border bg-secondary/40 p-3 animate-[twofaIn_0.3s_ease-out_0.15s_both] motion-reduce:animate-none">
        <div className="flex items-center justify-between">
          <span className="font-mono text-[10px] uppercase tracking-[0.15em] text-muted-foreground">Kurtarma kodları</span>
          <button
            onClick={() => setRevealed((r) => !r)}
            className="font-mono text-[9px] uppercase tracking-wider text-accent hover:underline focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring rounded-sm"
          >
            {revealed ? "GİZLE" : "GÖSTER"}
          </button>
        </div>
        <div className={cn("mt-2 grid grid-cols-2 gap-1 transition-all duration-300 motion-reduce:transition-none", revealed ? "opacity-100" : "opacity-0 select-none blur-sm")}>
          {codes.map((c, i) => (
            <code key={c} className="font-mono text-[11px] text-foreground/80" style={{ animationDelay: `${i * 30}ms` }}>{c}</code>
          ))}
        </div>
        <button
          onClick={copyAll}
          className="mt-2 w-full rounded-sm border border-dashed border-border py-1 font-mono text-[9px] uppercase tracking-wider text-muted-foreground transition-colors hover:border-foreground/40 hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring motion-reduce:transition-none"
        >
          {copied ? "✓ KOPYALANDI" : "TÜMÜNÜ KOPYALA"}
        </button>
      </div>

      <button
        onClick={onConfirm}
        className={cn(
          "w-full rounded-md bg-accent px-4 py-2.5 text-sm font-semibold uppercase tracking-widest text-accent-foreground",
          "transition-all duration-150 hover:bg-accent/90 active:scale-[0.98]",
          "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none",
          "animate-[twofaIn_0.3s_ease-out_0.25s_both] motion-reduce:animate-none"
        )}
      >
        Etkinleştir
      </button>
    </div>
  );
}

Props

PropTypeDescription
recoveryCodesstring[]kurtarma kodları; boşsa üretilmiş örnek gösterilir
onConfirm() => void