{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "credit-card-form",
  "type": "registry:block",
  "title": "Credit card form",
  "description": "Card number, name, expiry and security code, formatted as typed and checked on blur, with a card drawn above that fills in.",
  "author": "Box Kite <https://www.box-kite.dev>",
  "categories": [
    "ecommerce",
    "checkout",
    "form"
  ],
  "dependencies": [
    "@box-kite/react"
  ],
  "registryDependencies": [
    "https://www.box-kite.dev/r/store-design.json"
  ],
  "files": [
    {
      "path": "registry/designs/store/credit-card-form.tsx",
      "content": "'use client';\nimport Box from '@box-kite/react';\nimport Flex from '@box-kite/react/components/flex';\nimport Grid from '@box-kite/react/components/grid';\nimport { Span } from '@box-kite/react/components/semantics';\nimport Textbox from '@box-kite/react/components/textbox';\nimport { useState } from 'react';\nimport FormField from './form-field';\n\ntype CardField = 'number' | 'name' | 'expiry' | 'cvc';\n\nconst digits = (value: string) => value.replace(/\\D/g, '');\n\n/** Groups of four as the reader types, which is how the number is printed on the card in their hand. */\nconst formatCardNumber = (value: string) =>\n  digits(value)\n    .slice(0, 19)\n    .replace(/(\\d{4})(?=\\d)/g, '$1 ');\n\n/** `MM / YY`, with the slash written for them once the month is whole. */\nconst formatExpiry = (value: string) => {\n  const raw = digits(value).slice(0, 4);\n\n  return raw.length > 2 ? `${raw.slice(0, 2)} / ${raw.slice(2)}` : raw;\n};\n\n/** The Luhn checksum every card number carries: catches a mistyped or transposed digit before a server is asked. */\nfunction passesLuhn(value: string) {\n  const number = digits(value);\n  if (number.length < 12) return false;\n\n  let sum = 0;\n  for (let index = 0; index < number.length; index++) {\n    let digit = Number(number[number.length - 1 - index]);\n    if (index % 2) digit = digit * 2 > 9 ? digit * 2 - 9 : digit * 2;\n    sum += digit;\n  }\n\n  return sum % 10 === 0;\n}\n\nfunction problemWith(field: CardField, value: string, today = new Date()): string | undefined {\n  if (!value.trim()) return 'Required.';\n  if (field === 'number' && !passesLuhn(value)) return 'That card number does not look right — check the digits.';\n  if (field === 'cvc' && !/^\\d{3,4}$/.test(value)) return 'Three digits on the back, four on the front of some cards.';\n  if (field === 'expiry') {\n    const [month, year] = value.split(' / ').map(Number);\n    if (!month || month > 12 || !year) return 'Write it as MM / YY.';\n    if (new Date(2000 + year, month) <= today) return 'This card has expired.';\n  }\n\n  return undefined;\n}\n\n/**\n * A card payment fieldset: number, name, expiry and security code, formatted as they are typed and checked\n * when a field is left, with a card drawn above that fills in as they go. It is a `<fieldset>`, not a form,\n * so it drops into a checkout form beside the address; the fields submit as `cardNumber`, `cardName`,\n * `cardExpiry` and `cardCvc`, with the `autocomplete` tokens a browser fills a saved card from.\n *\n * This only shapes the input. Sending a raw card number to your own server puts you in scope for PCI DSS —\n * in production these fields are usually your payment provider's hosted inputs, laid out the same way.\n */\nexport default function CreditCardForm() {\n  const [values, setValues] = useState<Record<CardField, string>>({ number: '', name: '', expiry: '', cvc: '' });\n  const [touched, setTouched] = useState<Partial<Record<CardField, boolean>>>({});\n\n  const error = (field: CardField) => (touched[field] ? problemWith(field, values[field]) : undefined);\n  const set = (field: CardField, value: string) => setValues((current) => ({ ...current, [field]: value }));\n  const leave = (field: CardField) => () => setTouched((current) => ({ ...current, [field]: true }));\n\n  return (\n    <Box tag=\"fieldset\" b={0} p={0} m={0} minWidth={0}>\n      <Box tag=\"legend\" fontSize={16} fontWeight={500} mb={5}>\n        Payment\n      </Box>\n\n      <Card number={values.number} name={values.name} expiry={values.expiry} />\n\n      <Flex d=\"column\" gap={4} mt={5}>\n        <FormField label=\"Card number\" error={error('number')}>\n          {(control) => (\n            <Textbox\n              name=\"cardNumber\"\n              value={values.number}\n              onChange={(event) => set('number', formatCardNumber(event.target.value))}\n              placeholder=\"1234 5678 9012 3456\"\n              props={{ ...control, inputMode: 'numeric', autoComplete: 'cc-number', onBlur: leave('number') }}\n            />\n          )}\n        </FormField>\n        <FormField label=\"Name on card\" error={error('name')}>\n          {(control) => (\n            <Textbox\n              name=\"cardName\"\n              value={values.name}\n              onChange={(event) => set('name', event.target.value)}\n              props={{ ...control, autoComplete: 'cc-name', onBlur: leave('name') }}\n            />\n          )}\n        </FormField>\n        <Grid gridTemplateColumns={2} gap={4}>\n          <FormField label=\"Expiry\" error={error('expiry')}>\n            {(control) => (\n              <Textbox\n                name=\"cardExpiry\"\n                value={values.expiry}\n                onChange={(event) => set('expiry', formatExpiry(event.target.value))}\n                placeholder=\"MM / YY\"\n                props={{ ...control, inputMode: 'numeric', autoComplete: 'cc-exp', onBlur: leave('expiry') }}\n              />\n            )}\n          </FormField>\n          <FormField label=\"Security code\" error={error('cvc')}>\n            {(control) => (\n              <Textbox\n                name=\"cardCvc\"\n                value={values.cvc}\n                onChange={(event) => set('cvc', digits(event.target.value).slice(0, 4))}\n                placeholder=\"123\"\n                props={{ ...control, inputMode: 'numeric', autoComplete: 'cc-csc', onBlur: leave('cvc') }}\n              />\n            )}\n          </FormField>\n        </Grid>\n      </Flex>\n    </Box>\n  );\n}\n\n/** The card as it fills in. Decoration: every character on it is already in a labelled field below. */\nfunction Card({ number, name, expiry }: { number: string; name: string; expiry: string }) {\n  const shown = digits(number)\n    .padEnd(16, '•')\n    .replace(/(.{4})(?=.)/g, '$1 ');\n\n  return (\n    <Flex\n      d=\"column\"\n      jc=\"space-between\"\n      aspectRatio={1.586}\n      maxWidth={88}\n      p={5}\n      borderRadius={3}\n      color=\"stone-100\"\n      bgColor=\"stone-900\"\n      theme={{ dark: { bgColor: 'stone-800', ring: 1, ringColor: 'white/10' } }}\n      props={{ 'aria-hidden': true }}\n    >\n      <Flex jc=\"space-between\" ai=\"start\">\n        <Box width={9} height={7} borderRadius={1} bgColor=\"stone-400\" />\n        <Span fontSize={12} fontWeight={500} letterSpacing={2} color=\"stone-400\">\n          DEBIT\n        </Span>\n      </Flex>\n      <Span fontSize={20} letterSpacing={2} css={{ fontVariantNumeric: 'tabular-nums' }}>\n        {shown}\n      </Span>\n      <Flex jc=\"space-between\" fontSize={12} textTransform=\"uppercase\" letterSpacing={1}>\n        <Span overflow=\"hidden\" textOverflow=\"ellipsis\" whiteSpace=\"nowrap\">\n          {name || 'Your name'}\n        </Span>\n        <Span>{expiry || 'MM / YY'}</Span>\n      </Flex>\n    </Flex>\n  );\n}\n",
      "type": "registry:block",
      "target": "@components/box-kite/store/credit-card-form.tsx"
    }
  ],
  "docs": "Render <CreditCardForm /> inside your checkout form. In production, raw card numbers belong in your payment provider's hosted fields. Wrap it in <StoreSurface> from store-design.tsx, or render it inside one of the store pages."
}
