> ## Documentation Index
> Fetch the complete documentation index at: https://docs.rinne.com.br/llms.txt
> Use this file to discover all available pages before exploring further.

# Card Element

> Collect encrypted card data with validation state, field events, and theme customization.

export const RinneCardPresetsDemo = () => {
  const previewRef = useRef(null);
  const mountedCardRef = useRef(null);
  const [activePreset, setActivePreset] = useState('clean');
  const [isLoading, setIsLoading] = useState(true);
  const [initError, setInitError] = useState('');
  const [isDark, setIsDark] = useState(() => {
    if (typeof window !== 'undefined') {
      const html = document.documentElement;
      return html.classList.contains('dark') || html.getAttribute('data-theme') === 'dark' || window.matchMedia('(prefers-color-scheme: dark)').matches;
    }
    return false;
  });
  const shell = isDark ? {
    bg: '#05070b',
    panelBg: '#05070b',
    border: 'rgba(255, 255, 255, 0.18)',
    divider: 'rgba(255, 255, 255, 0.16)',
    text: '#f8fafc',
    textMuted: '#a1a1aa',
    tabText: '#9ca3af',
    tabActiveText: '#111827',
    tabActiveBg: '#ffffff',
    codeBg: '#05070b',
    codeText: '#d1d5db',
    codeNumber: '#4b5563',
    codeKeyword: '#7dd3fc',
    codeCall: '#fca5a5',
    codeProperty: '#93c5fd',
    codeString: '#86efac'
  } : {
    bg: '#ffffff',
    panelBg: '#ffffff',
    border: 'rgba(2, 6, 23, 0.2)',
    divider: 'rgba(2, 6, 23, 0.12)',
    text: '#0f172a',
    textMuted: '#64748b',
    tabText: '#64748b',
    tabActiveText: '#0f172a',
    tabActiveBg: '#ffffff',
    codeBg: '#f8fafc',
    codeText: '#334155',
    codeNumber: '#94a3b8',
    codeKeyword: '#0369a1',
    codeCall: '#b91c1c',
    codeProperty: '#1d4ed8',
    codeString: '#166534'
  };
  const lineNumberStyle = {
    color: shell.codeNumber,
    width: '24px',
    userSelect: 'none',
    flexShrink: 0
  };
  useEffect(() => {
    const detectTheme = () => {
      const html = document.documentElement;
      const isDarkMode = html.classList.contains('dark') || html.getAttribute('data-theme') === 'dark';
      setIsDark(isDarkMode);
    };
    detectTheme();
    const observer = new MutationObserver(detectTheme);
    observer.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ['class', 'data-theme']
    });
    const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
    mediaQuery.addEventListener('change', detectTheme);
    return () => {
      observer.disconnect();
      mediaQuery.removeEventListener('change', detectTheme);
    };
  }, []);
  useEffect(() => {
    const existing = document.querySelector('style[data-rinne-card-demo="true"]');
    if (existing) return;
    const styleSheet = document.createElement('style');
    styleSheet.dataset.rinneCardDemo = 'true';
    styleSheet.textContent = `
      @keyframes rinne-card-shimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } }
    `;
    document.head.appendChild(styleSheet);
    return () => {
      const sheet = document.querySelector('style[data-rinne-card-demo="true"]');
      if (sheet) {
        sheet.remove();
      }
    };
  }, []);
  useEffect(() => {
    let mounted = true;
    const unmountCurrent = () => {
      if (!mountedCardRef.current) return;
      try {
        mountedCardRef.current.unmount();
      } catch {}
      mountedCardRef.current = null;
    };
    const loadScript = () => {
      return new Promise((resolve, reject) => {
        if (window.Rinne) {
          resolve(window.Rinne);
          return;
        }
        const existing = document.querySelector('script[data-rinne-umd="true"]');
        if (existing) {
          existing.addEventListener('load', () => resolve(window.Rinne), {
            once: true
          });
          existing.addEventListener('error', () => reject(new Error('Failed to load Rinne script')), {
            once: true
          });
          return;
        }
        const script = document.createElement('script');
        script.src = 'https://pkgs.rinne.com.br/rinne-js';
        script.async = true;
        script.dataset.rinneUmd = 'true';
        script.onload = () => {
          if (window.Rinne) {
            resolve(window.Rinne);
          } else {
            reject(new Error('Rinne not found after script load'));
          }
        };
        script.onerror = () => reject(new Error('Failed to load Rinne script'));
        document.head.appendChild(script);
      });
    };
    const mountPreset = async () => {
      try {
        setIsLoading(true);
        setInitError('');
        unmountCurrent();
        const RinneClass = await loadScript();
        if (!mounted) return;
        const rinne = new RinneClass({
          merchantId: '730d1a4f-85ba-42c4-bf15-035dea51057c',
          environment: 'sandbox',
          rinneUrl: 'https://api-dev.rinne.com.br/core/v1'
        });
        if (!previewRef.current) {
          throw new Error('Preset preview container is not ready');
        }
        const cardElement = await rinne.elements.card({
          theme: activePreset,
          colorScheme: 'light',
          locale: 'pt',
          fields: ['number', 'expiry', 'cvc'],
          icons: true
        });
        const mountedCard = await cardElement.mount(previewRef.current);
        if (!mounted) {
          mountedCard.unmount();
          return;
        }
        mountedCardRef.current = mountedCard;
      } catch (error) {
        if (!mounted) return;
        setInitError(error instanceof Error ? error.message : 'Failed to initialize preset preview');
      } finally {
        if (mounted) {
          setIsLoading(false);
        }
      }
    };
    mountPreset();
    return () => {
      mounted = false;
      unmountCurrent();
    };
  }, [activePreset]);
  return <div style={{
    background: shell.bg,
    borderRadius: '12px',
    border: `1px solid ${shell.border}`,
    overflow: 'hidden',
    marginTop: '12px',
    marginBottom: '20px'
  }}>
      <div style={{
    display: 'flex',
    alignItems: 'center',
    gap: '8px',
    padding: '12px 14px',
    borderBottom: `1px solid ${shell.divider}`,
    background: shell.panelBg
  }}>
        {['clean', 'minimal', 'material'].map(preset => {
    const isActive = activePreset === preset;
    return <button key={preset} type="button" onClick={() => setActivePreset(preset)} style={{
      border: isActive ? '1px solid rgba(255, 255, 255, 0.24)' : '1px solid transparent',
      background: isActive ? shell.tabActiveBg : 'transparent',
      color: isActive ? shell.tabActiveText : shell.tabText,
      fontSize: '12px',
      fontWeight: 700,
      letterSpacing: '0.04em',
      textTransform: 'uppercase',
      borderRadius: '2px',
      padding: '6px 9px',
      cursor: 'pointer'
    }}>
              {preset}
            </button>;
  })}
      </div>

      <div style={{
    padding: '16px',
    borderBottom: `1px solid ${shell.divider}`
  }}>
        {initError && <div style={{
    fontSize: '13px',
    color: '#ef4444',
    border: '1px solid rgba(239, 68, 68, 0.35)',
    background: 'rgba(239, 68, 68, 0.08)',
    borderRadius: '8px',
    padding: '8px 10px',
    marginBottom: '12px'
  }}>
            {initError}
          </div>}

        <div style={{
    background: '#f5f6f8',
    border: '1px solid #e5e7eb',
    borderRadius: '10px',
    minHeight: '290px',
    padding: '34px',
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center'
  }}>
          <div style={{
    width: 'min(540px, 100%)',
    background: '#ffffff',
    borderRadius: '18px',
    border: '1px solid #e7ebf0',
    padding: '18px',
    boxShadow: '0 12px 24px rgba(15, 23, 42, 0.06)',
    minHeight: '190px',
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center'
  }}>
            <div style={{
    width: '100%',
    margin: '0 auto',
    minHeight: '135px',
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center'
  }}>
              <div ref={previewRef} style={{
    width: '100%',
    minHeight: '130px',
    position: 'relative'
  }}>
                {isLoading && <div style={{
    position: 'absolute',
    inset: 0,
    borderRadius: '12px',
    background: 'linear-gradient(90deg, rgba(0,0,0,0.04) 25%, rgba(0,0,0,0.08) 50%, rgba(0,0,0,0.04) 75%)',
    backgroundSize: '200% 100%',
    animation: 'rinne-card-shimmer 1.5s infinite'
  }} />}
              </div>
            </div>
          </div>
        </div>
      </div>

      <div style={{
    padding: '12px 14px',
    background: shell.codeBg
  }}>
        <pre style={{
    margin: 0,
    fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
    fontSize: '13px',
    color: shell.codeText,
    overflowX: 'auto',
    lineHeight: 1.55,
    whiteSpace: 'pre',
    background: isDark ? '#02040a' : '#f8fafc',
    border: isDark ? '1px solid rgba(255, 255, 255, 0.12)' : '1px solid #e2e8f0',
    borderRadius: '8px',
    padding: '12px 14px'
  }}>
          <code>
            <div style={{
    display: 'flex',
    whiteSpace: 'pre'
  }}>
              <span style={lineNumberStyle}>1</span>
              <span>
                <span style={{
    color: shell.codeKeyword
  }}>const</span>
                <span style={{
    color: shell.codeText
  }}> cardElement = </span>
                <span style={{
    color: shell.codeKeyword
  }}>await</span>
                <span style={{
    color: shell.codeText
  }}> </span>
                <span style={{
    color: shell.codeCall
  }}>rinne.elements.card</span>
                <span style={{
    color: shell.codeText
  }}>({`{`}</span>
              </span>
            </div>
            <div style={{
    display: 'flex',
    whiteSpace: 'pre'
  }}>
              <span style={lineNumberStyle}>2</span>
              <span>
                <span style={{
    color: shell.codeText
  }}>  </span>
                <span style={{
    color: shell.codeProperty
  }}>theme</span>
                <span style={{
    color: shell.codeText
  }}>: </span>
                <span style={{
    color: shell.codeString
  }}>{`'${activePreset}'`}</span>
                <span style={{
    color: shell.codeText
  }}>,</span>
              </span>
            </div>
            <div style={{
    display: 'flex',
    whiteSpace: 'pre'
  }}>
              <span style={lineNumberStyle}>3</span>
              <span>
                <span style={{
    color: shell.codeText
  }}>  </span>
                <span style={{
    color: shell.codeProperty
  }}>colorScheme</span>
                <span style={{
    color: shell.codeText
  }}>: </span>
                <span style={{
    color: shell.codeString
  }}>'light'</span>
                <span style={{
    color: shell.codeText
  }}>,</span>
              </span>
            </div>
            <div style={{
    display: 'flex',
    whiteSpace: 'pre'
  }}>
              <span style={lineNumberStyle}>4</span>
              <span>
                <span style={{
    color: shell.codeText
  }}>  </span>
                <span style={{
    color: shell.codeProperty
  }}>locale</span>
                <span style={{
    color: shell.codeText
  }}>: </span>
                <span style={{
    color: shell.codeString
  }}>'pt'</span>
                <span style={{
    color: shell.codeText
  }}>,</span>
              </span>
            </div>
            <div style={{
    display: 'flex',
    whiteSpace: 'pre'
  }}>
              <span style={lineNumberStyle}>5</span>
              <span>
                <span style={{
    color: shell.codeText
  }}>  </span>
                <span style={{
    color: shell.codeProperty
  }}>fields</span>
                <span style={{
    color: shell.codeText
  }}>: </span>
                <span style={{
    color: shell.codeString
  }}>['number', 'expiry', 'cvc']</span>
              </span>
            </div>
            <div style={{
    display: 'flex',
    whiteSpace: 'pre'
  }}>
              <span style={lineNumberStyle}>6</span>
              <span style={{
    color: shell.codeText
  }}>{`});`}</span>
            </div>
            <div style={{
    display: 'flex',
    whiteSpace: 'pre'
  }}>
              <span style={lineNumberStyle}>7</span>
              <span style={{
    color: shell.codeText
  }} />
            </div>
            <div style={{
    display: 'flex',
    whiteSpace: 'pre'
  }}>
              <span style={lineNumberStyle}>8</span>
              <span>
                <span style={{
    color: shell.codeKeyword
  }}>await</span>
                <span style={{
    color: shell.codeText
  }}> cardElement.mount(</span>
                <span style={{
    color: shell.codeString
  }}>'#card-element'</span>
                <span style={{
    color: shell.codeText
  }}>{`);`}</span>
              </span>
            </div>
          </code>
        </pre>
      </div>
    </div>;
};

export const RinneCardDemo = () => {
  const cardRef = useRef(null);
  const mountedCardRef = useRef(null);
  const isDarkFromDom = () => {
    const html = document.documentElement;
    return html.classList.contains('dark') || html.getAttribute('data-theme') === 'dark';
  };
  const [isLoading, setIsLoading] = useState(true);
  const [initError, setInitError] = useState('');
  const [isDark, setIsDark] = useState(() => {
    if (typeof window !== 'undefined') {
      const html = document.documentElement;
      return html.classList.contains('dark') || html.getAttribute('data-theme') === 'dark' || window.matchMedia('(prefers-color-scheme: dark)').matches;
    }
    return false;
  });
  const [cardState, setCardState] = useState({
    isValid: false,
    isComplete: false,
    errors: {}
  });
  const [encryptedSnapshot, setEncryptedSnapshot] = useState({
    card: {
      number: '',
      cvv: '',
      expiry_month: '',
      expiry_year: '',
      brand: '',
      last_digits: ''
    }
  });
  const getColors = () => {
    if (isDark) {
      return {
        bg: 'linear-gradient(180deg, #0f0f14 0%, #1a1a24 100%)',
        cardBg: 'linear-gradient(135deg, #1e1e2e 0%, #2d2d3f 100%)',
        headerBg: 'linear-gradient(90deg, rgba(99, 102, 241, 0.1) 0%, rgba(139, 92, 246, 0.1) 100%)',
        border: 'rgba(255, 255, 255, 0.06)',
        borderAccent: 'rgba(99, 102, 241, 0.2)',
        text: '#e4e4e7',
        textMuted: '#a1a1aa',
        textDim: '#6b7280',
        previewBg: 'linear-gradient(180deg, rgba(99, 102, 241, 0.03) 0%, transparent 100%)',
        panelBg: 'rgba(255, 255, 255, 0.03)',
        successBg: 'rgba(16, 185, 129, 0.12)',
        successBorder: 'rgba(16, 185, 129, 0.35)',
        stateCodeBg: 'rgba(2, 6, 23, 0.55)',
        stateCodeText: '#e2e8f0',
        encryptedCodeBg: 'rgba(2, 6, 23, 0.55)',
        encryptedCodeText: '#a7f3d0'
      };
    }
    return {
      bg: 'linear-gradient(180deg, #ffffff 0%, #f8fafc 100%)',
      cardBg: 'linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%)',
      headerBg: 'linear-gradient(90deg, rgba(99, 102, 241, 0.05) 0%, rgba(139, 92, 246, 0.05) 100%)',
      border: 'rgba(0, 0, 0, 0.08)',
      borderAccent: 'rgba(99, 102, 241, 0.15)',
      text: '#1e293b',
      textMuted: '#64748b',
      textDim: '#94a3b8',
      previewBg: 'linear-gradient(180deg, rgba(99, 102, 241, 0.02) 0%, transparent 100%)',
      panelBg: 'rgba(0, 0, 0, 0.02)',
      successBg: 'rgba(16, 185, 129, 0.1)',
      successBorder: 'rgba(16, 185, 129, 0.25)',
      stateCodeBg: 'rgba(255, 255, 255, 0.86)',
      stateCodeText: '#0f172a',
      encryptedCodeBg: 'rgba(16, 185, 129, 0.08)',
      encryptedCodeText: '#10b981'
    };
  };
  const colors = getColors();
  const PlayIcon = () => <svg width="10" height="10" viewBox="0 0 24 24" fill="currentColor">
      <polygon points="5 3 19 12 5 21 5 3" />
    </svg>;
  const LockIcon = () => <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
      <rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
      <path d="M7 11V7a5 5 0 0 1 10 0v4" />
    </svg>;
  useEffect(() => {
    const detectTheme = () => {
      setIsDark(isDarkFromDom());
    };
    detectTheme();
    const observer = new MutationObserver(detectTheme);
    observer.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ['class', 'data-theme']
    });
    const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
    const mediaHandler = () => detectTheme();
    mediaQuery.addEventListener('change', mediaHandler);
    return () => {
      observer.disconnect();
      mediaQuery.removeEventListener('change', mediaHandler);
    };
  }, []);
  useEffect(() => {
    const existing = document.querySelector('style[data-rinne-card-demo="true"]');
    if (existing) return;
    const styleSheet = document.createElement('style');
    styleSheet.dataset.rinneCardDemo = 'true';
    styleSheet.textContent = `
      @keyframes rinne-card-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
      @keyframes rinne-card-shimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } }
    `;
    document.head.appendChild(styleSheet);
    return () => {
      const sheet = document.querySelector('style[data-rinne-card-demo="true"]');
      if (sheet) {
        sheet.remove();
      }
    };
  }, []);
  useEffect(() => {
    let mounted = true;
    const maskCardValues = values => {
      if (!values || !values.card) return null;
      const safe = JSON.parse(JSON.stringify(values));
      if (safe.card.number) safe.card.number = '[ENCRYPTED]';
      if (safe.card.cvv) safe.card.cvv = '[ENCRYPTED]';
      return safe;
    };
    const pullSnapshot = () => {
      try {
        const values = mountedCardRef.current?.values;
        const masked = maskCardValues(values);
        if (masked && mounted) {
          setEncryptedSnapshot(masked);
        }
      } catch (error) {
        if (mounted) {
          setInitError(error instanceof Error ? error.message : 'Failed to read encrypted values');
        }
      }
    };
    const loadScript = () => {
      return new Promise((resolve, reject) => {
        if (window.Rinne) {
          resolve(window.Rinne);
          return;
        }
        const existing = document.querySelector('script[data-rinne-umd="true"]');
        if (existing) {
          existing.addEventListener('load', () => resolve(window.Rinne), {
            once: true
          });
          existing.addEventListener('error', () => reject(new Error('Failed to load Rinne script')), {
            once: true
          });
          return;
        }
        const script = document.createElement('script');
        script.src = 'https://pkgs.rinne.com.br/rinne-js';
        script.async = true;
        script.dataset.rinneUmd = 'true';
        script.onload = () => {
          if (window.Rinne) {
            resolve(window.Rinne);
          } else {
            reject(new Error('Rinne not found after script load'));
          }
        };
        script.onerror = () => reject(new Error('Failed to load Rinne script'));
        document.head.appendChild(script);
      });
    };
    const getCurrentColorScheme = () => {
      return isDarkFromDom() ? 'dark' : 'light';
    };
    const initDemo = async () => {
      try {
        setIsLoading(true);
        setInitError('');
        const RinneClass = await loadScript();
        if (!mounted) return;
        const rinne = new RinneClass({
          merchantId: '730d1a4f-85ba-42c4-bf15-035dea51057c',
          environment: 'sandbox',
          rinneUrl: 'https://api-dev.rinne.com.br/core/v1'
        });
        const card = await rinne.elements.card({
          theme: 'material',
          colorScheme: getCurrentColorScheme(),
          locale: 'pt',
          fields: ['number', 'expiry', 'cvc'],
          icons: true,
          onReady: () => {
            if (!mounted) return;
            setInitError('');
          },
          onChange: nextState => {
            if (!mounted) return;
            setCardState(nextState);
            pullSnapshot();
          }
        });
        if (!cardRef.current) {
          throw new Error('Card container not found');
        }
        const mountedCard = await card.mount(cardRef.current);
        if (!mounted) {
          mountedCard.unmount();
          return;
        }
        mountedCardRef.current = mountedCard;
        pullSnapshot();
      } catch (error) {
        if (!mounted) return;
        setInitError(error instanceof Error ? error.message : 'Failed to initialize card preview');
      } finally {
        if (mounted) {
          setIsLoading(false);
        }
      }
    };
    initDemo();
    return () => {
      mounted = false;
      if (mountedCardRef.current) {
        mountedCardRef.current.unmount();
        mountedCardRef.current = null;
      }
    };
  }, [isDark]);
  return <div style={{
    background: colors.bg,
    borderRadius: '16px',
    overflow: 'hidden',
    boxShadow: isDark ? '0 25px 50px -12px rgba(0, 0, 0, 0.5), 0 0 0 1px rgba(255, 255, 255, 0.05)' : '0 25px 50px -12px rgba(0, 0, 0, 0.1), 0 0 0 1px rgba(0, 0, 0, 0.05)',
    marginTop: '16px',
    marginBottom: '24px'
  }}>
      <div style={{
    background: colors.headerBg,
    borderBottom: `1px solid ${colors.border}`,
    padding: '10px 16px',
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'space-between'
  }}>
        <div style={{
    color: colors.textMuted,
    fontSize: '11px',
    fontWeight: '600',
    textTransform: 'uppercase',
    letterSpacing: '1px',
    display: 'flex',
    alignItems: 'center',
    gap: '8px'
  }}>
          <span style={{
    color: '#10b981'
  }}>
            <PlayIcon />
          </span>
          Live Card Preview
        </div>

        <div style={{
    display: 'flex',
    alignItems: 'center',
    gap: '6px',
    background: 'rgba(16, 185, 129, 0.15)',
    padding: '4px 10px',
    borderRadius: '9999px',
    border: '1px solid rgba(16, 185, 129, 0.3)'
  }}>
          <div style={{
    width: '6px',
    height: '6px',
    borderRadius: '50%',
    background: '#10b981',
    animation: 'rinne-card-pulse 2s infinite'
  }} />
          <span style={{
    color: '#10b981',
    fontSize: '11px',
    fontWeight: '600',
    textTransform: 'uppercase',
    letterSpacing: '0.5px'
  }}>
            Sandbox
          </span>
        </div>
      </div>

      <div style={{
    padding: '14px 16px',
    background: colors.previewBg
  }}>
        {initError && <div style={{
    fontSize: '13px',
    color: '#ef4444',
    border: '1px solid rgba(239, 68, 68, 0.35)',
    background: 'rgba(239, 68, 68, 0.1)',
    borderRadius: '10px',
    padding: '10px 12px',
    marginBottom: '12px'
  }}>
            {initError}
          </div>}

        <div style={{
    display: 'flex',
    gap: '16px',
    flexWrap: 'wrap'
  }}>
          <div style={{
    flex: '1 1 380px',
    background: colors.cardBg,
    borderRadius: '12px',
    padding: '16px',
    border: `1px solid ${colors.borderAccent}`
  }}>
            <div style={{
    color: colors.textDim,
    fontSize: '11px',
    fontWeight: '600',
    textTransform: 'uppercase',
    letterSpacing: '0.5px',
    marginBottom: '12px',
    display: 'flex',
    alignItems: 'center',
    gap: '6px'
  }}>
              <LockIcon />
              Card Input
            </div>

            <div ref={cardRef} style={{
    width: '100%',
    minHeight: '150px',
    position: 'relative'
  }}>
              {isLoading && <div style={{
    position: 'absolute',
    inset: 0,
    borderRadius: '8px',
    background: isDark ? 'linear-gradient(90deg, rgba(255,255,255,0.06) 25%, rgba(255,255,255,0.1) 50%, rgba(255,255,255,0.06) 75%)' : 'linear-gradient(90deg, rgba(0,0,0,0.04) 25%, rgba(0,0,0,0.08) 50%, rgba(0,0,0,0.04) 75%)',
    backgroundSize: '200% 100%',
    animation: 'rinne-card-shimmer 1.5s infinite'
  }} />}
            </div>
          </div>

          <div style={{
    flex: '1 1 280px',
    display: 'flex',
    flexDirection: 'column',
    gap: '12px'
  }}>
            <div style={{
    background: colors.panelBg,
    borderRadius: '12px',
    padding: '14px',
    border: `1px solid ${colors.border}`
  }}>
              <div style={{
    color: colors.textDim,
    fontSize: '10px',
    fontWeight: '600',
    textTransform: 'uppercase',
    letterSpacing: '0.5px',
    marginBottom: '8px'
  }}>
                State
              </div>
              <pre style={{
    margin: 0,
    fontSize: '11px',
    color: colors.stateCodeText,
    WebkitTextFillColor: colors.stateCodeText,
    fontFamily: 'ui-monospace, monospace',
    overflow: 'auto',
    maxHeight: '190px',
    whiteSpace: 'pre-wrap',
    wordBreak: 'break-word',
    background: colors.stateCodeBg,
    borderRadius: '10px',
    border: `1px solid ${colors.border}`,
    padding: '10px',
    lineHeight: 1.45
  }}>
                {JSON.stringify(cardState, null, 2)}
              </pre>
            </div>

            <div style={{
    background: colors.panelBg,
    border: `1px solid ${colors.border}`,
    borderRadius: '12px',
    padding: '14px'
  }}>
              <div style={{
    color: '#10b981',
    fontSize: '10px',
    fontWeight: '600',
    textTransform: 'uppercase',
    letterSpacing: '0.5px',
    marginBottom: '8px'
  }}>
                Encrypted Values
              </div>

              <pre style={{
    margin: 0,
    fontSize: '11px',
    color: colors.encryptedCodeText,
    WebkitTextFillColor: colors.encryptedCodeText,
    fontFamily: 'ui-monospace, monospace',
    overflow: 'auto',
    maxHeight: '190px',
    whiteSpace: 'pre-wrap',
    wordBreak: 'break-word',
    background: colors.stateCodeBg,
    borderRadius: '10px',
    border: `1px solid ${colors.border}`,
    padding: '10px',
    lineHeight: 1.45
  }}>
                {JSON.stringify(encryptedSnapshot, null, 2)}
              </pre>
            </div>
          </div>
        </div>
      </div>

      <div style={{
    borderTop: `1px solid ${colors.border}`,
    padding: '10px 20px',
    background: isDark ? 'rgba(0, 0, 0, 0.2)' : 'rgba(0, 0, 0, 0.02)',
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'space-between'
  }}>
        <span style={{
    color: colors.textDim,
    fontSize: '11px'
  }}>
          <span style={{
    color: '#6366f1'
  }}>@rinnebr/js</span> card element
        </span>
        <div style={{
    display: 'flex',
    alignItems: 'center',
    gap: '8px',
    color: colors.textDim,
    fontSize: '11px'
  }}>
          <span>Encrypted payload preview</span>
          <span style={{
    opacity: 0.3
  }}>·</span>
          <span>Sandbox mode</span>
        </div>
      </div>
    </div>;
};

Card Element is the secure card entry component in rinne-js. It renders sensitive fields in an isolated context and returns encrypted values and validation state for your checkout UI.

Use Card Element for all card transactions. Rinne expects encrypted values from `mountedCard.values` and does not accept plain PAN/CVC.

## Create a Card Element

```typescript theme={null}
const cardElement = await rinne.elements.card({
  theme: 'material',
  locale: 'pt',
  fields: ['name', 'number', 'expiry', 'cvc'],
  icons: true
})

const mountedCard = await cardElement.mount('#card-element')
```

<Warning>
  The Core API requires `cardholder_name` and `last_digits` (4 digits) when creating card transactions. Include `'name'` in your `fields` array so the cardholder name is captured by the Card Element. If you omit `'name'`, you must collect the cardholder name through your own form input and pass it as `cardholder_name` in `card_data` when calling the API.
</Warning>

<Warning>
  To process real card transactions, the merchant or organization must have an active Rinne affiliation for card processing.
</Warning>

<a id="mounted-card-contract" />

## Capture Payload Contract

`cardElement.mount()` returns a `mountedCard` object. In production checkout, send `mountedCard.values.card` to your backend.

```typescript theme={null}
interface MountedCard {
  isValid: boolean
  isComplete: boolean
  values: {
    card: {
      number: string // encrypted
      cvv: string // encrypted
      expiry_month: string // zero-padded, e.g. '01'
      expiry_year: string // four digits, e.g. '2026'
      cardholder_name?: string
      brand?: string
      last_digits?: string
    }
  }
  unmount(): void
}
```

Before the user finishes input, fields in `mountedCard.values.card` can still be empty strings.

<Warning>
  `number` and `cvv` are encrypted in `mountedCard.values.card`. `expiry_month` and `expiry_year` are plain strings.
</Warning>

## Options

```typescript theme={null}
interface CardElementOptions {
  theme?: 'clean' | 'minimal' | 'material' | ThemeConfig | ThemeFunction
  colorScheme?: 'light' | 'dark' // iframe root color-scheme; match your page theme
  locale?: 'pt' | 'en' | 'es' // default: 'pt'
  icons?: boolean
  fields?: ('name' | 'number' | 'expiry' | 'cvc')[]
  onReady?: () => void
  onChange?: (state: CardState) => void
  onComplete?: (state: CardState) => void
  onFocus?: (event: CardFieldEvent) => void
  onBlur?: (event: CardFieldEvent) => void
}
```

## Event Lifecycle

| Event               | When it fires                          | Recommended use                         |
| ------------------- | -------------------------------------- | --------------------------------------- |
| `onReady()`         | Card iframe is mounted and interactive | Hide loading state                      |
| `onChange(state)`   | Any input/value/validation change      | Keep submit button and UI state in sync |
| `onComplete(state)` | All configured fields are filled       | Trigger pre-submit checks               |
| `onFocus(event)`    | A field receives focus                 | Clear stale field-level errors          |
| `onBlur(event)`     | A field loses focus                    | Show field-level validation message     |

<Info>
  `onComplete` means all fields are filled. It does not guarantee the values are valid.
</Info>

## colorScheme

Set `colorScheme` to the same value as your page theme so the card iframe blends correctly with light or dark surfaces.

```typescript theme={null}
const colorScheme =
  document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light'

const cardElement = await rinne.elements.card({
  theme: 'material',
  colorScheme,
  locale: 'pt',
  fields: ['number', 'expiry', 'cvc']
})
```

## Field Control

```typescript theme={null}
// Recommended: include cardholder name (required by Core API)
await rinne.elements.card({ fields: ['name', 'number', 'expiry', 'cvc'] })

// Without cardholder name (you must provide cardholder_name separately in card_data)
await rinne.elements.card({ fields: ['number', 'expiry', 'cvc'] })
```

## Validation State

```typescript theme={null}
interface CardState {
  isValid: boolean
  isComplete: boolean
  errors: {
    number?: string
    expiry?: string
    cvc?: string
    name?: string
  }
}
```

Use `onChange` to enable/disable your submit button.

```typescript theme={null}
const cardElement = await rinne.elements.card({
  onChange: (state) => {
    submitButton.disabled = !state.isComplete || !state.isValid
  }
})
```

## Read Mounted Card Values

After mount, you can read encrypted values from `mountedCard.values`.

```typescript theme={null}
if (mountedCard.isComplete && mountedCard.isValid) {
  const payload = mountedCard.values

  // Encrypted values
  console.log(payload.card.number)
  console.log(payload.card.cvv)

  // Additional metadata
  console.log(payload.card.brand)
  console.log(payload.card.last_digits)
}
```

<Check>
  Card number and CVV are encrypted before exposure in `mountedCard.values`.
</Check>

## Submit Pattern

```typescript theme={null}
const mountedCard = await cardElement.mount('#card-element')

async function submitCardCheckout() {
  if (!mountedCard.isComplete || !mountedCard.isValid) {
    return
  }

  await fetch('/api/checkout/card', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ cardData: mountedCard.values.card })
  })
}
```

<Warning>
  Never collect raw PAN/CVC in your own inputs. Use only encrypted values from `mountedCard.values`.
</Warning>

## Live Demo

This sandbox demo mounts the card element directly in Mintlify, applies `colorScheme` based on the current docs theme, and shows real-time state plus encrypted values.

<RinneCardDemo />

## Theming

`theme` accepts:

* A preset: `'clean' | 'minimal' | 'material'`
* A config object: `{ preset?, styles?, fonts? }`
* A function theme: `(utils) => ({ styles, fonts })`

## Preset Preview

This live sandbox preview uses tabs to switch between built-in presets while keeping the same
card configuration. The iframe window stays on a light surface for easier visual comparison.

<RinneCardPresetsDemo />

## Custom Styling

<Info>
  Theme styles are CSS-in-JS rules injected into the secure iframe. You can style CSS selectors, but you cannot change iframe HTML structure.
</Info>

### Full Theme Example

```typescript rinne-theme.ts theme={null}
export const checkoutTheme = {
  preset: 'minimal',
  fonts: ['https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap'],
  styles: {
    ':root': {
      colorScheme: 'light'
    },
    fieldset: {
      border: '1px solid #dbe2ea',
      borderRadius: '14px',
      backgroundColor: '#ffffff',
      padding: '14px'
    },
    label: {
      color: '#475569',
      fontSize: 12,
      fontWeight: 600,
      textTransform: 'uppercase',
      letterSpacing: '0.04em'
    },
    '.field input': {
      marginTop: 6,
      height: '48px',
      padding: '10px 12px',
      border: '1px solid #cbd5e1',
      borderRadius: '10px',
      backgroundColor: '#ffffff',
      color: '#0f172a',
      fontSize: 16
    },
    '.field[ev-name="number"] input': {
      letterSpacing: '0.08em',
      fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace'
    },
    '.field:focus-within input': {
      borderColor: '#2563eb',
      boxShadow: '0 0 0 3px rgba(37, 99, 235, 0.15)'
    },
    '.field[ev-valid="false"] input': {
      borderColor: '#dc2626'
    },
    '.field[ev-has-value="true"] input': {
      backgroundColor: '#f8fafc'
    },
    '.error': {
      marginTop: 6,
      fontSize: 12,
      color: '#dc2626'
    }
  }
}
```

```typescript theme={null}
const cardElement = await rinne.elements.card({
  colorScheme: 'light',
  theme: checkoutTheme,
  locale: 'pt',
  fields: ['number', 'expiry', 'cvc']
})
```

### State Attributes for Selectors

Use these iframe attributes for state-aware styling:

| Scope                  | Attribute      | Values                            |
| ---------------------- | -------------- | --------------------------------- |
| Root `fieldset`        | `ev-component` | `Card`                            |
| Root `fieldset`        | `ev-valid`     | `true` or `false`                 |
| Field wrapper `.field` | `ev-name`      | `name`, `number`, `expiry`, `cvc` |
| Field wrapper `.field` | `ev-valid`     | `true` or `false`                 |
| Field wrapper `.field` | `ev-has-value` | `true` or `false`                 |

## Responsive Styling

Use a function theme and `utils.media()` so breakpoints match the parent document viewport.

```typescript theme={null}
const responsiveTheme = (utils) => ({
  fonts: ['https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap'],
  styles: {
    label: {
      fontSize: 12
    },
    '.field input': {
      height: '48px',
      padding: '10px 12px',
      fontSize: 16
    },
    '.field[ev-name="number"] input': {
      letterSpacing: '0.08em'
    },
    ...utils.media('(max-width: 768px)', {
      '.field input': {
        height: '44px',
        fontSize: 15
      }
    }),
    ...utils.media('(max-width: 480px)', {
      label: {
        fontSize: 11
      },
      '.field input': {
        height: '40px',
        padding: '8px 10px',
        fontSize: 14
      },
      '.field[ev-name="number"] input': {
        letterSpacing: '0.05em'
      }
    })
  }
})
```

```typescript theme={null}
const cardElement = await rinne.elements.card({
  colorScheme: document.documentElement.classList.contains('dark') ? 'dark' : 'light',
  theme: responsiveTheme
})
```

<Tip>
  Set both `colorScheme` and `theme` using the same page theme source so iframe and host container stay visually consistent.
</Tip>

## Mount Errors

`mount()` throws when the target selector is invalid or the provider cannot render the component.

```typescript theme={null}
try {
  const card = await rinne.elements.card()
  await card.mount('#missing-card-slot')
} catch (error) {
  console.error('Card mount failed', error)
}
```

## Unmount

```typescript theme={null}
mountedCard.unmount()
```
