{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "address-form",
  "title": "Address Form",
  "author": "ecomcn <hello@ecomcn.dev>",
  "description": "A shipping or billing address whose fields follow the country: a state and ZIP code for the US, a town and postcode for the UK, family name first for Japan, a generic shape for the rest. Country and region are your shadcn Combobox, searchable, and every field carries its section-prefixed autocomplete token, so browser autofill fills the lot. validateAddress is a pure function in its own file, so your server runs the same rules. Error summary that links to each field, an address-lookup slot, save-as-default, a loading skeleton. Fields that come and go with the country grow and fold to their measured height (Motion; a fade under reduced motion).",
  "dependencies": [
    "motion",
    "lucide-react"
  ],
  "registryDependencies": [
    "combobox",
    "field",
    "input",
    "checkbox",
    "skeleton"
  ],
  "files": [
    {
      "path": "src/registry/ecomcn/address-form/address-form.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { CircleAlert } from \"lucide-react\"\nimport { AnimatePresence, MotionConfig, animate, motion, useReducedMotion } from \"motion/react\"\n\nimport { Checkbox } from \"@/components/ui/checkbox\"\nimport {\n  Combobox,\n  ComboboxContent,\n  ComboboxEmpty,\n  ComboboxInput,\n  ComboboxItem,\n  ComboboxList,\n} from \"@/components/ui/combobox\"\nimport { Field, FieldError, FieldLabel, FieldLegend, FieldSet } from \"@/components/ui/field\"\nimport { Input } from \"@/components/ui/input\"\nimport { Skeleton } from \"@/components/ui/skeleton\"\nimport {\n  COUNTRY_CODES,\n  getAddressFormat,\n  type Address,\n  type AddressErrors,\n  type AddressFieldModes,\n  type AddressFieldName,\n  type AddressFormat,\n  type AddressFormatInput,\n} from \"@/lib/address-format\"\nimport { cn } from \"@/lib/utils\"\n\n/*\n * A shipping or billing address whose fields follow the country: a US\n * address asks for a state and a ZIP code, a UK one for a town and a\n * postcode, a Japanese one starts with the family name.\n *\n *   const [address, setAddress] = React.useState(emptyAddress(\"US\"))\n *   <AddressForm value={address} onValueChange={setAddress} errors={errors} />\n *\n *   <AddressForm value={address} onValueChange={setAddress}>          or composed\n *     <AddressFormErrorSummary />\n *     <AddressFormCountry />\n *     <AddressFormFields />\n *   </AddressForm>\n *\n * Country and region are your shadcn Combobox — type \"ger\" for Germany —\n * and every field carries its `autocomplete` token, so browser autofill\n * fills the lot, country and state included. Validation is yours to run:\n * `validateAddress` from `@/lib/address-format` is a pure function, so the\n * server checks with the same rules; pass what it returns as `errors`.\n * Fields that come and go with the country grow and fold to their measured\n * height, and the lists grow out of their field (Motion; a fade under\n * reduced motion).\n */\n\nexport type { Address, AddressErrors, AddressFieldModes, AddressFieldName, AddressFormat } from \"@/lib/address-format\"\n\n/** The same settle curve as the other ecomcn morphs: no bounce, an editorial ease. */\nconst MORPH = { duration: 0.45, ease: [0.2, 0, 0, 1] } as const\nconst FADE = { duration: 0.15 } as const\n\nconst subscribeNothing = () => () => {}\n\n/** Your locale, or the visitor's once hydrated — so server and client HTML agree. */\nfunction useFormatLocale(locale?: string) {\n  const hydrated = React.useSyncExternalStore(subscribeNothing, () => true, () => false)\n  return locale ?? (hydrated ? undefined : \"en-US\")\n}\n\n/** Each field's autofill token, after the section (\"shipping\" / \"billing\"). */\nconst AUTOCOMPLETE: Record<AddressFieldName, string> = {\n  givenName: \"given-name\",\n  familyName: \"family-name\",\n  organization: \"organization\",\n  line1: \"address-line1\",\n  line2: \"address-line2\",\n  city: \"address-level2\",\n  region: \"address-level1\",\n  postalCode: \"postal-code\",\n  phone: \"tel\",\n}\n\n/** Side by side once the form is wide enough — the form's width, not the screen's. */\nconst COLUMNS: Record<number, string> = {\n  1: \"\",\n  2: \"@sm/address-form:grid-cols-2\",\n  3: \"@sm/address-form:grid-cols-2 @md/address-form:grid-cols-3\",\n}\n\n/** Grows to its measured height and folds back; a plain fade under reduced motion. */\nfunction fold(reduce: boolean | null) {\n  return reduce\n    ? { initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 }, transition: FADE }\n    : {\n        initial: { height: 0, opacity: 0, overflow: \"hidden\" },\n        // Visible once open, so focus rings aren't clipped.\n        animate: { height: \"auto\", opacity: 1, transitionEnd: { overflow: \"visible\" } },\n        exit: { height: 0, opacity: 0, overflow: \"hidden\" },\n        transition: MORPH,\n      }\n}\n\n/** \"State\" -> \"state\", but \"ZIP code\" keeps its capitals. */\nconst lower = (label: string) => (/^[A-Z][a-z]/.test(label) ? label[0].toLowerCase() + label.slice(1) : label)\n\ntype Option = { value: string; label: string }\n\n/** Lower case, accents off: \"cote\" finds \"Côte d’Ivoire\". */\nconst plain = (text: string) => text.normalize(\"NFD\").replace(/\\p{M}/gu, \"\").toLowerCase()\n\n/**\n * Best match first: the code itself (\"de\"), then names that start with what\n * was typed (\"ger\" -> Germany), then a word inside the name (\"kingdom\"),\n * then anywhere — so \"ger\" doesn't open on Algeria.\n */\nfunction rank(options: Option[], query: string) {\n  const q = plain(query.trim())\n  if (!q) return options\n  const scored: { option: Option; score: number; index: number }[] = []\n  options.forEach((option, index) => {\n    const label = plain(option.label)\n    const at = label.indexOf(q)\n    let score = -1\n    if (option.value.toLowerCase() === q) score = 0\n    else if (at === 0) score = 1\n    else if (at > 0 && /[\\s(\\-’']/.test(label[at - 1])) score = 2\n    else if (at > 0) score = 3\n    if (score !== -1) scored.push({ option, score, index })\n  })\n  return scored.sort((a, b) => a.score - b.score || a.index - b.index).map((s) => s.option)\n}\n\n/* ─── context ──────────────────────────────────────────────────────────── */\n\ntype Part = AddressFieldName | \"country\"\n\nexport interface AddressFormContextValue {\n  value: Address\n  /** The format for the chosen country, with your `formats` and `fields` applied. */\n  format: AddressFormat\n  /** Sets one field. A new country drops a region it doesn't have. */\n  setField: (field: Part, next: string) => void\n  /** Sets several fields at once — what an address lookup calls with its result. */\n  setAddress: (patch: Partial<Address>) => void\n  /** The errors on show: yours, less any field the shopper has edited since. */\n  errors: AddressErrors\n  section: \"shipping\" | \"billing\"\n  /** The id of a field's input, for a label or a link of your own. */\n  fieldId: (field: Part) => string\n  /** Moves focus to a field and scrolls it into view. */\n  focusField: (field: Part) => void\n  countries: Option[]\n  name?: string\n  lookup?: React.ReactNode\n  saveAsDefault: boolean\n  /** Undefined when the form was given no `onSaveAsDefaultChange`. */\n  setSaveAsDefault?: (checked: boolean) => void\n  disabled: boolean\n  loading: boolean\n}\n\nconst AddressFormContext = React.createContext<AddressFormContextValue | null>(null)\n\nexport function useAddressForm() {\n  const context = React.useContext(AddressFormContext)\n  if (!context) {\n    throw new Error(\"useAddressForm must be used within <AddressForm>.\")\n  }\n  return context\n}\n\n/* ─── root ─────────────────────────────────────────────────────────────── */\n\nexport interface AddressFormProps\n  extends Omit<React.FieldsetHTMLAttributes<HTMLFieldSetElement>, \"onChange\" | \"defaultValue\" | \"name\" | \"children\"> {\n  value: Address\n  onValueChange: (address: Address) => void\n  /** Prefixes every autocomplete token, so a billing form never autofills as shipping. */\n  section?: \"shipping\" | \"billing\"\n  /** Messages by field — from `validateAddress`, your schema or your server. */\n  errors?: AddressErrors | null\n  /** ISO codes to offer, in your order. Every country, by name, when left out. */\n  countries?: string[]\n  /** Your countries, or changes to ours — see `AddressFormatInput`. */\n  formats?: Record<string, Partial<AddressFormatInput>>\n  /** Show, hide or require the company, second line and phone in every country. */\n  fields?: AddressFieldModes\n  /** For country names. `undefined` follows the visitor's locale, once hydrated. */\n  locale?: string\n  /** \"Shipping address\" or \"Billing address\", by default. */\n  legend?: React.ReactNode\n  /** Keeps the legend for screen readers when your page already has a heading. */\n  hideLegend?: boolean\n  /** An address search — Google Places, Loqate, your own — placed above the fields. */\n  lookup?: React.ReactNode\n  saveAsDefault?: boolean\n  /** Shows \"Save as my default address\"; leave it out to hide the checkbox. */\n  onSaveAsDefaultChange?: (checked: boolean) => void\n  /** Names every input `${name}.city` and so on, for a plain form post. */\n  name?: string\n  loading?: boolean\n  children?: React.ReactNode\n}\n\nexport function AddressForm({\n  value,\n  onValueChange,\n  section = \"shipping\",\n  errors,\n  countries,\n  formats,\n  fields,\n  locale: localeProp,\n  legend,\n  hideLegend = false,\n  lookup,\n  saveAsDefault = false,\n  onSaveAsDefaultChange,\n  name,\n  loading = false,\n  disabled = false,\n  className,\n  children,\n  ...props\n}: AddressFormProps) {\n  const locale = useFormatLocale(localeProp)\n  const reduce = useReducedMotion()\n  const uid = React.useId()\n\n  const formatOptions = React.useMemo(() => ({ formats, fields }), [formats, fields])\n  const format = React.useMemo(() => getAddressFormat(value.country ?? \"\", formatOptions), [value.country, formatOptions])\n\n  // Autofill fires a change per field in one tick, before React re-renders;\n  // building each from the last one sent keeps all of them.\n  const latest = React.useRef(value)\n  React.useLayoutEffect(() => {\n    latest.current = value\n  }, [value])\n  const emit = React.useCallback(\n    (next: Address) => {\n      latest.current = next\n      onValueChange(next)\n    },\n    [onValueChange]\n  )\n\n  // A field's error stays until the shopper edits it, or new errors arrive.\n  const errorsKey = JSON.stringify(errors ?? {})\n  const [edited, setEdited] = React.useState({ errorsKey, fields: new Set<Part>() })\n  if (edited.errorsKey !== errorsKey) setEdited({ errorsKey, fields: new Set() })\n  const visibleErrors = React.useMemo(() => {\n    const shown: AddressErrors = {}\n    for (const [field, message] of Object.entries(errors ?? {}) as [Part, string | undefined][]) {\n      if (message && !edited.fields.has(field)) shown[field] = message\n    }\n    return shown\n  }, [errors, edited])\n  const markEdited = React.useCallback(\n    (parts: Part[]) =>\n      setEdited((prev) => (parts.every((p) => prev.fields.has(p)) ? prev : { ...prev, fields: new Set([...prev.fields, ...parts]) })),\n    []\n  )\n\n  const setAddress = React.useCallback(\n    (patch: Partial<Address>) => {\n      const current = latest.current\n      const next = { ...current, ...patch }\n      if (patch.country !== undefined && patch.country !== current.country && patch.region === undefined) {\n        // A region only carries over between countries that both type it in.\n        const from = getAddressFormat(current.country ?? \"\", formatOptions).regions\n        const to = getAddressFormat(patch.country, formatOptions).regions\n        const keeps = to ? to.some((r) => r.code === current.region) : !from\n        if (!keeps) next.region = \"\"\n      }\n      markEdited(Object.keys(patch) as Part[])\n      emit(next)\n    },\n    [emit, formatOptions, markEdited]\n  )\n  const setField = React.useCallback((field: Part, next: string) => setAddress({ [field]: next }), [setAddress])\n\n  const fieldId = React.useCallback((field: Part) => `${uid}-${field}`, [uid])\n  const focusField = React.useCallback(\n    (field: Part) => {\n      const input = document.getElementById(fieldId(field))\n      if (!input) return\n      input.focus({ preventScroll: true })\n      input.scrollIntoView({ block: \"center\", behavior: reduce ? \"auto\" : \"smooth\" })\n    },\n    [fieldId, reduce]\n  )\n\n  const countryOptions = useCountryOptions(countries, value.country, locale)\n\n  const context = React.useMemo<AddressFormContextValue>(\n    () => ({\n      value,\n      format,\n      setField,\n      setAddress,\n      errors: visibleErrors,\n      section,\n      fieldId,\n      focusField,\n      countries: countryOptions,\n      name,\n      lookup,\n      saveAsDefault,\n      setSaveAsDefault: onSaveAsDefaultChange,\n      disabled,\n      loading,\n    }),\n    [value, format, setField, setAddress, visibleErrors, section, fieldId, focusField, countryOptions, name, lookup, saveAsDefault, onSaveAsDefaultChange, disabled, loading]\n  )\n\n  return (\n    <AddressFormContext.Provider value={context}>\n      <MotionConfig reducedMotion=\"user\">\n        <FieldSet\n          data-slot=\"address-form\"\n          data-section={section}\n          disabled={disabled}\n          aria-busy={loading || undefined}\n          className={cn(\"@container/address-form min-w-0 gap-5\", className)}\n          {...props}\n        >\n          <FieldLegend className={cn(hideLegend && \"sr-only\")}>\n            {legend ?? (section === \"billing\" ? \"Billing address\" : \"Shipping address\")}\n          </FieldLegend>\n          {loading ? (\n            <AddressFormSkeleton />\n          ) : (\n            (children ?? (\n              <>\n                <AddressFormErrorSummary />\n                <AddressFormCountry />\n                <AddressFormLookup />\n                <AddressFormFields />\n                <AddressFormSaveDefault />\n              </>\n            ))\n          )}\n        </FieldSet>\n      </MotionConfig>\n    </AddressFormContext.Provider>\n  )\n}\n\n/** Country names in the visitor's language: every country by name, or yours in your order. */\nfunction useCountryOptions(codes: string[] | undefined, current: string | undefined, locale: string | undefined) {\n  const key = codes?.join(\",\")\n  return React.useMemo(() => {\n    let names: Intl.DisplayNames | null = null\n    try {\n      names = new Intl.DisplayNames(locale ? [locale] : undefined, { type: \"region\" })\n    } catch {\n      // Very old engines: show the codes.\n    }\n    const list = key ? key.split(\",\") : [...COUNTRY_CODES]\n    if (current && !list.includes(current.toUpperCase())) list.push(current.toUpperCase())\n    const options = list.map((code) => ({ value: code.toUpperCase(), label: names?.of(code.toUpperCase()) ?? code }))\n    if (!key) options.sort((a, b) => a.label.localeCompare(b.label, locale))\n    return options\n  }, [key, current, locale])\n}\n\n/* ─── a combobox that grows out of its field ───────────────────────────── */\n\nconst OPEN_CLIP = \"inset(-10% -10% -10% -10%)\"\n\ninterface AddressComboboxProps {\n  id: string\n  options: Option[]\n  value: string\n  onValueChange: (value: string) => void\n  autoComplete: string\n  name?: string\n  invalid: boolean\n  describedBy?: string\n  required: boolean\n  disabled: boolean\n  empty: string\n}\n\n/**\n * Your shadcn Combobox, searchable, with its list growing out of the field\n * it belongs to and folding back into it. Base UI is told to wait for the\n * fold before it unmounts the list.\n */\nfunction AddressCombobox({\n  id,\n  options,\n  value,\n  onValueChange,\n  autoComplete,\n  name,\n  invalid,\n  describedBy,\n  required,\n  disabled,\n  empty,\n}: AddressComboboxProps) {\n  const reduce = useReducedMotion()\n  const popupId = React.useId()\n  const actionsRef = React.useRef<{ unmount: () => void; close: () => void } | null>(null)\n  const phase = React.useRef<\"closed\" | \"opening\" | \"open\" | \"closing\">(\"closed\")\n  const running = React.useRef<{ stop: () => void } | null>(null)\n  const frame = React.useRef(0)\n\n  const selected = React.useMemo(() => options.find((o) => o.value === value) ?? null, [options, value])\n  const [query, setQuery] = React.useState(\"\")\n  const filtered = React.useMemo(() => rank(options, query), [options, query])\n  const findPopup = () => document.querySelector<HTMLElement>(`[data-address-popup=\"${popupId}\"]`)\n\n  /** `fromHere` reverses a fold already under way rather than restarting it. */\n  const play = (popup: HTMLElement, open: boolean, fromHere = false) => {\n    running.current?.stop()\n    if (reduce) {\n      const controls = animate(popup, { opacity: open ? 1 : 0 }, FADE)\n      running.current = controls\n      return controls\n    }\n    // From the field's edge: down from below it, up from above it.\n    const shut = popup.getAttribute(\"data-side\") === \"top\" ? \"inset(100% -10% -10% -10%)\" : \"inset(-10% -10% 100% -10%)\"\n    const controls = animate(\n      popup,\n      { clipPath: open && !fromHere ? [shut, OPEN_CLIP] : open ? OPEN_CLIP : shut },\n      MORPH\n    )\n    running.current = controls\n    return controls\n  }\n\n  // The list mounts a frame or two after the open request, in a portal.\n  const playOpen = (tries = 12) => {\n    cancelAnimationFrame(frame.current)\n    const popup = findPopup()\n    if (!popup) {\n      if (tries > 0) frame.current = requestAnimationFrame(() => playOpen(tries - 1))\n      return\n    }\n    const reversing = phase.current === \"closing\"\n    phase.current = \"opening\"\n    play(popup, true, reversing).then(() => {\n      if (phase.current === \"opening\") phase.current = \"open\"\n    })\n  }\n\n  React.useEffect(() => () => cancelAnimationFrame(frame.current), [])\n\n  return (\n    <Combobox\n      items={options}\n      filteredItems={filtered}\n      onInputValueChange={(input: string) => setQuery(input)}\n      value={selected}\n      onValueChange={(next: Option | null) => onValueChange(next?.value ?? \"\")}\n      isItemEqualToValue={(a: Option, b: Option) => a.value === b.value}\n      autoHighlight\n      // The browser fills a hidden input with this token; Base UI matches\n      // \"US\" or \"United States\" to an option and selects it.\n      autoComplete={autoComplete}\n      name={name}\n      disabled={disabled}\n      actionsRef={actionsRef}\n      onOpenChange={(open, details) => {\n        if (open) {\n          playOpen()\n          return\n        }\n        cancelAnimationFrame(frame.current)\n        const popup = findPopup()\n        if (!popup) return\n        details.preventUnmountOnClose()\n        phase.current = \"closing\"\n        play(popup, false).then(() => {\n          if (phase.current !== \"closing\") return\n          phase.current = \"closed\"\n          actionsRef.current?.unmount()\n        })\n      }}\n    >\n      <ComboboxInput\n        id={id}\n        className=\"w-full\"\n        disabled={disabled}\n        aria-invalid={invalid || undefined}\n        aria-describedby={describedBy}\n        aria-required={required || undefined}\n      />\n      <ComboboxContent\n        data-address-popup={popupId}\n        className={cn(\n          // Motion runs the open and close; the primitive's keyframes would fight it.\n          \"animate-none!\",\n          reduce\n            ? \"opacity-0\"\n            : \"[clip-path:inset(-10%_-10%_100%_-10%)] data-[side=top]:[clip-path:inset(100%_-10%_-10%_-10%)]\"\n        )}\n      >\n        <ComboboxEmpty>{empty}</ComboboxEmpty>\n        <ComboboxList className=\"[scrollbar-width:none] [&::-webkit-scrollbar]:hidden\">\n          {(option: Option) => (\n            <ComboboxItem key={option.value} value={option}>\n              {option.label}\n            </ComboboxItem>\n          )}\n        </ComboboxList>\n      </ComboboxContent>\n    </Combobox>\n  )\n}\n\n/* ─── parts ────────────────────────────────────────────────────────────── */\n\nfunction FieldLabelText({ label, required }: { label: string; required: boolean }) {\n  return (\n    <>\n      {label}\n      {!required && <span className=\"font-normal text-muted-foreground\">(optional)</span>}\n    </>\n  )\n}\n\nexport interface AddressFormCountryProps extends React.HTMLAttributes<HTMLDivElement> {\n  /** \"Country / region\", by default. */\n  label?: string\n}\n\n/** The country, first: it decides every field below it. */\nexport function AddressFormCountry({ label = \"Country / region\", className, ...props }: AddressFormCountryProps) {\n  const { value, countries, setField, errors, fieldId, section, name, disabled } = useAddressForm()\n  const id = fieldId(\"country\")\n  const error = errors.country\n  return (\n    <Field\n      data-slot=\"address-form-country\"\n      data-invalid={error ? true : undefined}\n      data-disabled={disabled || undefined}\n      className={className}\n      {...props}\n    >\n      <FieldLabel htmlFor={id}>{label}</FieldLabel>\n      <AddressCombobox\n        id={id}\n        options={countries}\n        value={value.country ?? \"\"}\n        onValueChange={(next) => setField(\"country\", next)}\n        autoComplete={`${section} country`}\n        name={name ? `${name}.country` : undefined}\n        invalid={!!error}\n        describedBy={error ? `${id}-error` : undefined}\n        required\n        disabled={disabled}\n        empty=\"No country matches\"\n      />\n      <FieldError id={`${id}-error`} role={undefined}>\n        {error}\n      </FieldError>\n    </Field>\n  )\n}\n\nexport interface AddressFormFieldProps extends React.HTMLAttributes<HTMLDivElement> {\n  field: AddressFieldName\n}\n\n/** One field, labelled and wired: its autofill token, its error, its keyboard. */\nexport function AddressFormField({ field, className, ...props }: AddressFormFieldProps) {\n  const { value, format, setField, errors, fieldId, section, name, disabled } = useAddressForm()\n  const id = fieldId(field)\n  const error = errors[field]\n  const required = format.required.includes(field)\n  const label = format.labels[field]\n  const describedBy = error ? `${id}-error` : undefined\n  const autoComplete = `${section} ${AUTOCOMPLETE[field]}`\n  const inputName = name ? `${name}.${field}` : undefined\n\n  const control =\n    field === \"region\" && format.regions ? (\n      <AddressCombobox\n        id={id}\n        options={format.regions.map((r) => ({ value: r.code, label: r.name }))}\n        value={value.region ?? \"\"}\n        onValueChange={(next) => setField(\"region\", next)}\n        autoComplete={autoComplete}\n        name={inputName}\n        invalid={!!error}\n        describedBy={describedBy}\n        required={required}\n        disabled={disabled}\n        empty={`No ${lower(label)} matches`}\n      />\n    ) : (\n      <Input\n        id={id}\n        name={inputName}\n        value={value[field] ?? \"\"}\n        onChange={(event) => setField(field, event.target.value)}\n        autoComplete={autoComplete}\n        aria-invalid={error ? true : undefined}\n        aria-describedby={describedBy}\n        aria-required={required || undefined}\n        disabled={disabled}\n        {...(field === \"phone\" && { type: \"tel\", inputMode: \"tel\" as const })}\n        {...(field === \"postalCode\" && {\n          inputMode: format.postalCode?.numeric ? (\"numeric\" as const) : undefined,\n          autoCapitalize: format.postalCode?.numeric ? undefined : \"characters\",\n          spellCheck: false,\n        })}\n        {...((field === \"givenName\" || field === \"familyName\" || field === \"city\") && { autoCapitalize: \"words\" })}\n      />\n    )\n\n  return (\n    <Field\n      data-slot=\"address-form-field\"\n      data-field={field}\n      data-invalid={error ? true : undefined}\n      data-disabled={disabled || undefined}\n      className={cn(\"min-w-0\", className)}\n      {...props}\n    >\n      <FieldLabel htmlFor={id}>\n        <FieldLabelText label={label} required={required} />\n      </FieldLabel>\n      {control}\n      <FieldError id={`${id}-error`} role={undefined}>\n        {error}\n      </FieldError>\n    </Field>\n  )\n}\n\n/**\n * The country's fields, row by row. A row the new country doesn't have\n * folds away where it stood and a new one grows in where it belongs; the\n * rows that stay move only as far as those heights carry them. No layout\n * slide on top: two motions on one row is how fields end up overlapping.\n */\nexport function AddressFormFields({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {\n  const { format } = useAddressForm()\n  const reduce = useReducedMotion()\n  const motionProps = fold(reduce)\n  return (\n    <div data-slot=\"address-form-fields\" className={cn(\"flex flex-col\", className)} {...props}>\n      <AnimatePresence initial={false}>\n        {format.rows.map((row) => (\n          <motion.div key={row.join(\"+\")} {...motionProps}>\n            <div className={cn(\"grid gap-4 pb-5\", COLUMNS[row.length] ?? COLUMNS[3])}>\n              {row.map((field) => (\n                <AddressFormField key={field} field={field} />\n              ))}\n            </div>\n          </motion.div>\n        ))}\n      </AnimatePresence>\n    </div>\n  )\n}\n\n/** Your address search, above the fields. Calls `useAddressForm().setAddress` with its result. */\nexport function AddressFormLookup({ children, className, ...props }: React.HTMLAttributes<HTMLDivElement>) {\n  const { lookup } = useAddressForm()\n  const content = children ?? lookup\n  if (!content) return null\n  return (\n    <div data-slot=\"address-form-lookup\" className={className} {...props}>\n      {content}\n    </div>\n  )\n}\n\nexport interface AddressFormSaveDefaultProps extends React.HTMLAttributes<HTMLLabelElement> {\n  /** \"Save as my default shipping address\", by default. */\n  label?: React.ReactNode\n}\n\n/** Shown only when the form was given `onSaveAsDefaultChange`. */\nexport function AddressFormSaveDefault({ label, className, ...props }: AddressFormSaveDefaultProps) {\n  const { saveAsDefault, setSaveAsDefault, section, disabled } = useAddressForm()\n  if (!setSaveAsDefault) return null\n  return (\n    <label\n      data-slot=\"address-form-save-default\"\n      className={cn(\"flex w-fit cursor-pointer items-center gap-3 text-sm\", disabled && \"cursor-not-allowed opacity-50\", className)}\n      {...props}\n    >\n      <Checkbox checked={saveAsDefault} disabled={disabled} onCheckedChange={(checked: boolean) => setSaveAsDefault(checked === true)} />\n      {label ?? `Save as my default ${section} address`}\n    </label>\n  )\n}\n\n/**\n * Every error at once, at the top, each a link to its field — for the\n * shopper who pressed continue at the bottom. It takes focus when it\n * appears, and folds away as the fields are fixed.\n */\nexport function AddressFormErrorSummary({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {\n  const { errors, format, focusField, fieldId } = useAddressForm()\n  const reduce = useReducedMotion()\n  const order: Part[] = [\"country\", ...format.rows.flat()]\n  const entries = order.flatMap((field) => (errors[field] ? [{ field, message: errors[field] as string }] : []))\n  return (\n    <AnimatePresence initial={false}>\n      {entries.length > 0 && (\n        <motion.div key=\"summary\" {...fold(reduce)}>\n          <ErrorSummaryBody\n            entries={entries}\n            focusField={focusField}\n            fieldId={fieldId}\n            className={className}\n            {...props}\n          />\n        </motion.div>\n      )}\n    </AnimatePresence>\n  )\n}\n\n/** Takes its entries as props, so it keeps them while the summary folds away. */\nfunction ErrorSummaryBody({\n  entries,\n  focusField,\n  fieldId,\n  className,\n  ...props\n}: React.HTMLAttributes<HTMLDivElement> & {\n  entries: { field: Part; message: string }[]\n  focusField: (field: Part) => void\n  fieldId: (field: Part) => string\n}) {\n  const ref = React.useRef<HTMLDivElement>(null)\n  React.useEffect(() => {\n    ref.current?.focus({ preventScroll: true })\n    ref.current?.scrollIntoView({ block: \"nearest\" })\n  }, [])\n  return (\n    <div\n      ref={ref}\n      role=\"alert\"\n      tabIndex={-1}\n      data-slot=\"address-form-error-summary\"\n      className={cn(\"mb-1 border-l-2 border-destructive py-1 pl-4 text-sm outline-none\", className)}\n      {...props}\n    >\n      <p className=\"flex items-center gap-2 font-medium text-destructive\">\n        <CircleAlert className=\"size-4 shrink-0\" aria-hidden />\n        {entries.length === 1 ? \"Check this field\" : `Check these ${entries.length} fields`}\n      </p>\n      <ul className=\"mt-2 flex flex-col gap-1\">\n        {entries.map(({ field, message }) => (\n          <li key={field}>\n            <a\n              href={`#${fieldId(field)}`}\n              onClick={(event) => {\n                event.preventDefault()\n                focusField(field)\n              }}\n              className=\"underline underline-offset-4 hover:text-destructive focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none\"\n            >\n              {message}\n            </a>\n          </li>\n        ))}\n      </ul>\n    </div>\n  )\n}\n\n/** The form's shape, while you load a saved address. */\nfunction AddressFormSkeleton() {\n  const { format } = useAddressForm()\n  const rows: string[][] = [[\"country\"], ...format.rows]\n  return (\n    <div data-slot=\"address-form-skeleton\" className=\"flex flex-col gap-5\">\n      <span className=\"sr-only\" role=\"status\">\n        Loading the address form\n      </span>\n      {rows.map((row) => (\n        <div key={row.join(\"+\")} aria-hidden className={cn(\"grid gap-4\", COLUMNS[row.length] ?? COLUMNS[3])}>\n          {row.map((field) => (\n            <div key={field} className=\"flex flex-col gap-3\">\n              <Skeleton className=\"h-3.5 w-24 rounded-none\" />\n              <Skeleton className=\"h-9 w-full rounded-none\" />\n            </div>\n          ))}\n        </div>\n      ))}\n    </div>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/ecomcn/address-form.tsx"
    },
    {
      "path": "src/registry/ecomcn/address-form/address-format.ts",
      "content": "/*\n * What an address looks like in each country, and the rules that check one.\n *\n *   const format = getAddressFormat(\"GB\")\n *   const errors = validateAddress(address, format)   // {} when it's fine\n *\n * No \"use client\" and no React: import it on the server and run the same\n * checks there that the form runs in the browser. `address-form` draws its\n * fields from these formats; pass `formats` to either to add a country or\n * change one.\n */\n\nexport type AddressFieldName =\n  | \"givenName\"\n  | \"familyName\"\n  | \"organization\"\n  | \"line1\"\n  | \"line2\"\n  | \"city\"\n  | \"region\"\n  | \"postalCode\"\n  | \"phone\"\n\nexport interface Address {\n  /** ISO 3166-1 alpha-2: \"US\", \"IN\". */\n  country: string\n  givenName: string\n  familyName: string\n  organization?: string\n  line1: string\n  line2?: string\n  city: string\n  /**\n   * With a region list, the ISO 3166-2 subdivision code without the country\n   * prefix — \"CA\" for California, \"MH\" for Maharashtra, \"13\" for Tokyo.\n   * Otherwise whatever the shopper typed.\n   */\n  region?: string\n  postalCode?: string\n  phone?: string\n}\n\n/** An error message for each field that needs fixing; empty when nothing does. */\nexport type AddressErrors = Partial<Record<AddressFieldName | \"country\", string>>\n\nexport interface AddressRegion {\n  code: string\n  name: string\n}\n\nexport interface AddressFormat {\n  /** Top to bottom; fields in a row sit side by side once the form is wide enough. */\n  rows: AddressFieldName[][]\n  /** Fields the shopper must fill. Every other field in `rows` is optional. */\n  required: AddressFieldName[]\n  labels: Record<AddressFieldName, string>\n  /** When set, the region is chosen from this list rather than typed. */\n  regions?: AddressRegion[]\n  postalCode?: {\n    pattern: RegExp\n    /** Shown in the error: \"Enter a valid ZIP code, like 94103\". */\n    example: string\n    /** Digits only, so phones show the number pad. */\n    numeric?: boolean\n  }\n}\n\n/** A country's format as you write it: labels you leave out keep their defaults. */\nexport type AddressFormatInput = Omit<AddressFormat, \"labels\"> & {\n  labels?: Partial<Record<AddressFieldName, string>>\n}\n\n/** Whether a field is shown, and whether it must be filled — over any country's format. */\nexport type AddressFieldMode = \"hidden\" | \"optional\" | \"required\"\nexport type AddressFieldModes = Partial<Record<\"organization\" | \"line2\" | \"phone\", AddressFieldMode>>\n\nexport const DEFAULT_ADDRESS_LABELS: Record<AddressFieldName, string> = {\n  givenName: \"First name\",\n  familyName: \"Last name\",\n  organization: \"Company\",\n  line1: \"Address\",\n  line2: \"Apartment, suite, etc.\",\n  city: \"City\",\n  region: \"Region\",\n  postalCode: \"Postal code\",\n  phone: \"Phone\",\n}\n\n/* ─── regions ──────────────────────────────────────────────────────────── */\n\nconst list = (entries: string): AddressRegion[] =>\n  entries\n    .trim()\n    .split(\"\\n\")\n    .map((line) => {\n      const [code, name] = line.trim().split(\"|\")\n      return { code, name }\n    })\n\nconst US_STATES = list(`\n  AL|Alabama\n  AK|Alaska\n  AS|American Samoa\n  AZ|Arizona\n  AR|Arkansas\n  CA|California\n  CO|Colorado\n  CT|Connecticut\n  DE|Delaware\n  DC|District of Columbia\n  FL|Florida\n  GA|Georgia\n  GU|Guam\n  HI|Hawaii\n  ID|Idaho\n  IL|Illinois\n  IN|Indiana\n  IA|Iowa\n  KS|Kansas\n  KY|Kentucky\n  LA|Louisiana\n  ME|Maine\n  MD|Maryland\n  MA|Massachusetts\n  MI|Michigan\n  MN|Minnesota\n  MS|Mississippi\n  MO|Missouri\n  MT|Montana\n  NE|Nebraska\n  NV|Nevada\n  NH|New Hampshire\n  NJ|New Jersey\n  NM|New Mexico\n  NY|New York\n  NC|North Carolina\n  ND|North Dakota\n  MP|Northern Mariana Islands\n  OH|Ohio\n  OK|Oklahoma\n  OR|Oregon\n  PA|Pennsylvania\n  PR|Puerto Rico\n  RI|Rhode Island\n  SC|South Carolina\n  SD|South Dakota\n  TN|Tennessee\n  TX|Texas\n  UT|Utah\n  VT|Vermont\n  VI|U.S. Virgin Islands\n  VA|Virginia\n  WA|Washington\n  WV|West Virginia\n  WI|Wisconsin\n  WY|Wyoming\n`)\n\nconst CA_PROVINCES = list(`\n  AB|Alberta\n  BC|British Columbia\n  MB|Manitoba\n  NB|New Brunswick\n  NL|Newfoundland and Labrador\n  NT|Northwest Territories\n  NS|Nova Scotia\n  NU|Nunavut\n  ON|Ontario\n  PE|Prince Edward Island\n  QC|Quebec\n  SK|Saskatchewan\n  YT|Yukon\n`)\n\nconst AU_STATES = list(`\n  ACT|Australian Capital Territory\n  NSW|New South Wales\n  NT|Northern Territory\n  QLD|Queensland\n  SA|South Australia\n  TAS|Tasmania\n  VIC|Victoria\n  WA|Western Australia\n`)\n\nconst IN_STATES = list(`\n  AN|Andaman and Nicobar Islands\n  AP|Andhra Pradesh\n  AR|Arunachal Pradesh\n  AS|Assam\n  BR|Bihar\n  CH|Chandigarh\n  CG|Chhattisgarh\n  DH|Dadra and Nagar Haveli and Daman and Diu\n  DL|Delhi\n  GA|Goa\n  GJ|Gujarat\n  HR|Haryana\n  HP|Himachal Pradesh\n  JK|Jammu and Kashmir\n  JH|Jharkhand\n  KA|Karnataka\n  KL|Kerala\n  LA|Ladakh\n  LD|Lakshadweep\n  MP|Madhya Pradesh\n  MH|Maharashtra\n  MN|Manipur\n  ML|Meghalaya\n  MZ|Mizoram\n  NL|Nagaland\n  OD|Odisha\n  PY|Puducherry\n  PB|Punjab\n  RJ|Rajasthan\n  SK|Sikkim\n  TN|Tamil Nadu\n  TS|Telangana\n  TR|Tripura\n  UP|Uttar Pradesh\n  UK|Uttarakhand\n  WB|West Bengal\n`)\n\n// North to south, the order Japanese forms list them in.\nconst JP_PREFECTURES = `Hokkaido Aomori Iwate Miyagi Akita Yamagata Fukushima Ibaraki Tochigi\n  Gunma Saitama Chiba Tokyo Kanagawa Niigata Toyama Ishikawa Fukui Yamanashi Nagano Gifu\n  Shizuoka Aichi Mie Shiga Kyoto Osaka Hyogo Nara Wakayama Tottori Shimane Okayama\n  Hiroshima Yamaguchi Tokushima Kagawa Ehime Kochi Fukuoka Saga Nagasaki Kumamoto Oita\n  Miyazaki Kagoshima Okinawa`\n  .split(/\\s+/)\n  .map((name, i) => ({ code: String(i + 1).padStart(2, \"0\"), name }))\n\n/* ─── formats ──────────────────────────────────────────────────────────── */\n\nconst NAMES: AddressFieldName[] = [\"givenName\", \"familyName\"]\nconst STREET: AddressFieldName[][] = [[\"line1\"], [\"line2\"]]\n\n/** Any country without its own entry: every field, nothing assumed. */\nexport const GENERIC_ADDRESS_FORMAT: AddressFormatInput = {\n  rows: [NAMES, ...STREET, [\"city\"], [\"region\", \"postalCode\"], [\"phone\"]],\n  required: [\"givenName\", \"familyName\", \"line1\", \"city\"],\n  labels: { region: \"State / province / region\" },\n}\n\nexport const ADDRESS_FORMATS: Record<string, AddressFormatInput> = {\n  US: {\n    rows: [NAMES, ...STREET, [\"city\", \"region\", \"postalCode\"], [\"phone\"]],\n    required: [\"givenName\", \"familyName\", \"line1\", \"city\", \"region\", \"postalCode\"],\n    labels: { region: \"State\", postalCode: \"ZIP code\" },\n    regions: US_STATES,\n    postalCode: { pattern: /^\\d{5}(?:[-\\s]?\\d{4})?$/, example: \"94103\", numeric: true },\n  },\n  CA: {\n    rows: [NAMES, ...STREET, [\"city\", \"region\", \"postalCode\"], [\"phone\"]],\n    required: [\"givenName\", \"familyName\", \"line1\", \"city\", \"region\", \"postalCode\"],\n    labels: { region: \"Province\" },\n    regions: CA_PROVINCES,\n    postalCode: { pattern: /^[ABCEGHJ-NPRSTVXY]\\d[ABCEGHJ-NPRSTV-Z][\\s-]?\\d[ABCEGHJ-NPRSTV-Z]\\d$/i, example: \"K1A 0B1\" },\n  },\n  GB: {\n    rows: [NAMES, ...STREET, [\"city\", \"postalCode\"], [\"phone\"]],\n    required: [\"givenName\", \"familyName\", \"line1\", \"city\", \"postalCode\"],\n    labels: { city: \"Town or city\", postalCode: \"Postcode\" },\n    postalCode: { pattern: /^(?:GIR\\s?0AA|[A-Z]{1,2}\\d[A-Z\\d]?\\s?\\d[A-Z]{2})$/i, example: \"SW1A 1AA\" },\n  },\n  AU: {\n    rows: [NAMES, ...STREET, [\"city\", \"region\", \"postalCode\"], [\"phone\"]],\n    required: [\"givenName\", \"familyName\", \"line1\", \"city\", \"region\", \"postalCode\"],\n    labels: { city: \"Suburb\", region: \"State\", postalCode: \"Postcode\" },\n    regions: AU_STATES,\n    postalCode: { pattern: /^\\d{4}$/, example: \"2000\", numeric: true },\n  },\n  IN: {\n    rows: [NAMES, ...STREET, [\"city\", \"region\", \"postalCode\"], [\"phone\"]],\n    // Couriers in India call before delivering.\n    required: [\"givenName\", \"familyName\", \"line1\", \"city\", \"region\", \"postalCode\", \"phone\"],\n    labels: { line2: \"Apartment, area, landmark\", region: \"State\", postalCode: \"PIN code\" },\n    regions: IN_STATES,\n    postalCode: { pattern: /^[1-9]\\d{2}\\s?\\d{3}$/, example: \"110001\", numeric: true },\n  },\n  DE: {\n    rows: [NAMES, ...STREET, [\"postalCode\", \"city\"], [\"phone\"]],\n    required: [\"givenName\", \"familyName\", \"line1\", \"city\", \"postalCode\"],\n    labels: { line1: \"Street and house number\", postalCode: \"Postcode\" },\n    postalCode: { pattern: /^\\d{5}$/, example: \"10115\", numeric: true },\n  },\n  FR: {\n    rows: [NAMES, ...STREET, [\"postalCode\", \"city\"], [\"phone\"]],\n    required: [\"givenName\", \"familyName\", \"line1\", \"city\", \"postalCode\"],\n    labels: { postalCode: \"Postcode\" },\n    postalCode: { pattern: /^\\d{2}\\s?\\d{3}$/, example: \"75001\", numeric: true },\n  },\n  JP: {\n    // Family name first, then the address from the largest unit down.\n    rows: [[\"familyName\", \"givenName\"], [\"postalCode\", \"region\"], [\"city\"], ...STREET, [\"phone\"]],\n    required: [\"familyName\", \"givenName\", \"postalCode\", \"region\", \"city\", \"line1\"],\n    labels: { region: \"Prefecture\", city: \"City, ward, town\", line1: \"Street address\", line2: \"Building, room\" },\n    regions: JP_PREFECTURES,\n    postalCode: { pattern: /^\\d{3}-?\\d{4}$/, example: \"100-0001\", numeric: true },\n  },\n}\n\n/** Every ISO 3166-1 alpha-2 code. Pass `countries` to the form to offer only those you ship to. */\nexport const COUNTRY_CODES: readonly string[] = `AD AE AF AG AI AL AM AO AQ AR AS AT AU AW AX AZ\n  BA BB BD BE BF BG BH BI BJ BL BM BN BO BQ BR BS BT BV BW BY BZ CA CC CD CF CG CH CI CK CL CM\n  CN CO CR CU CV CW CX CY CZ DE DJ DK DM DO DZ EC EE EG EH ER ES ET FI FJ FK FM FO FR GA GB GD\n  GE GF GG GH GI GL GM GN GP GQ GR GS GT GU GW GY HK HM HN HR HT HU ID IE IL IM IN IO IQ IR IS\n  IT JE JM JO JP KE KG KH KI KM KN KP KR KW KY KZ LA LB LC LI LK LR LS LT LU LV LY MA MC MD ME\n  MF MG MH MK ML MM MN MO MP MQ MR MS MT MU MV MW MX MY MZ NA NC NE NF NG NI NL NO NP NR NU NZ\n  OM PA PE PF PG PH PK PL PM PN PR PS PT PW PY QA RE RO RS RU RW SA SB SC SD SE SG SH SI SJ SK\n  SL SM SN SO SR SS ST SV SX SY SZ TC TD TF TG TH TJ TK TL TM TN TO TR TT TV TW TZ UA UG UM US\n  UY UZ VA VC VE VG VI VN VU WF WS YE YT ZA ZM ZW`.split(/\\s+/)\n\n/* ─── resolving a format ───────────────────────────────────────────────── */\n\nexport interface AddressFormatOptions {\n  /** Your countries, or changes to ours — merged over the built-in entry. */\n  formats?: Record<string, Partial<AddressFormatInput>>\n  /** Show, hide or require the company, second address line and phone everywhere. */\n  fields?: AddressFieldModes\n}\n\n/** Where a field goes back in when a mode turns it on. */\nfunction insertRow(rows: AddressFieldName[][], field: AddressFieldName): AddressFieldName[][] {\n  if (field === \"phone\") return [...rows, [\"phone\"]]\n  const anchor = field === \"line2\" ? \"line1\" : rows[0]?.find((f) => f === \"givenName\" || f === \"familyName\")\n  const at = rows.findIndex((row) => anchor !== undefined && row.includes(anchor))\n  return at === -1 ? [[field], ...rows] : [...rows.slice(0, at + 1), [field], ...rows.slice(at + 1)]\n}\n\n/** The format for a country — built-in, generic, or yours — with field modes applied. */\nexport function getAddressFormat(country: string, options: AddressFormatOptions = {}): AddressFormat {\n  const code = country.toUpperCase()\n  const base = ADDRESS_FORMATS[code] ?? GENERIC_ADDRESS_FORMAT\n  const own = options.formats?.[code]\n  const input: AddressFormatInput = { ...base, ...own, labels: { ...base.labels, ...own?.labels } }\n\n  let rows = input.rows.map((row) => [...row])\n  let required = [...input.required]\n  for (const [field, mode] of Object.entries(options.fields ?? {}) as [AddressFieldName, AddressFieldMode][]) {\n    const shown = rows.some((row) => row.includes(field))\n    if (mode === \"hidden\") {\n      rows = rows.map((row) => row.filter((f) => f !== field)).filter((row) => row.length > 0)\n      required = required.filter((f) => f !== field)\n      continue\n    }\n    if (!shown) rows = insertRow(rows, field)\n    required = mode === \"required\" ? [...new Set([...required, field])] : required.filter((f) => f !== field)\n  }\n\n  return {\n    rows,\n    required,\n    labels: { ...DEFAULT_ADDRESS_LABELS, ...input.labels },\n    regions: input.regions,\n    postalCode: input.postalCode,\n  }\n}\n\n/** An empty address in a country. */\nexport function emptyAddress(country = \"US\"): Address {\n  return { country, givenName: \"\", familyName: \"\", line1: \"\", city: \"\" }\n}\n\n/* ─── validation ───────────────────────────────────────────────────────── */\n\n/** \"State\" -> \"state\", but \"ZIP code\" and \"PIN code\" keep their capitals. */\nconst lower = (label: string) => (/^[A-Z][a-z]/.test(label) ? label[0].toLowerCase() + label.slice(1) : label)\nconst article = (word: string) => (/^[aeiou]/i.test(word) ? \"an\" : \"a\")\nconst named = (label: string) => `${article(lower(label))} ${lower(label)}`\n\nfunction missing(field: AddressFieldName, format: AddressFormat) {\n  const label = format.labels[field]\n  if (field === \"givenName\" || field === \"familyName\") return `Enter your ${lower(label)}`\n  if (field === \"phone\") return \"Enter a phone number\"\n  if (field === \"region\" && format.regions) return `Choose ${named(label)}`\n  return `Enter ${named(label)}`\n}\n\n/**\n * The same rules the form shows, as a pure function: required fields, the\n * postcode's shape, a region from the list, a plausible phone number. Returns\n * an error message per field, or `{}` when the address will do. Run it again\n * on your server — the browser's copy can be skipped.\n */\nexport function validateAddress(address: Address, format: AddressFormat = getAddressFormat(address.country)): AddressErrors {\n  const errors: AddressErrors = {}\n  if (!address.country?.trim()) errors.country = \"Choose a country\"\n\n  const shown = new Set(format.rows.flat())\n  for (const field of shown) {\n    const value = (address[field] ?? \"\").trim()\n    if (!value) {\n      if (format.required.includes(field)) errors[field] = missing(field, format)\n      continue\n    }\n    if (field === \"postalCode\" && format.postalCode && !format.postalCode.pattern.test(value)) {\n      errors.postalCode = `Enter a valid ${lower(format.labels.postalCode)}, like ${format.postalCode.example}`\n    }\n    if (field === \"region\" && format.regions && !format.regions.some((r) => r.code === value)) {\n      errors.region = `Choose ${named(format.labels.region)} from the list`\n    }\n    if (field === \"phone\" && !(/^\\+?[\\d\\s().-]+$/.test(value) && value.replace(/\\D/g, \"\").length >= 6)) {\n      errors.phone = \"Enter a valid phone number\"\n    }\n  }\n  return errors\n}\n\n/* ─── display ──────────────────────────────────────────────────────────── */\n\n/**\n * The address as lines for a label or a \"Ship to\" summary, in the country's\n * order: \"San Francisco, California 94103\", \"10115 Berlin\". Region codes\n * become names; the country itself is left to you.\n */\nexport function formatAddressLines(address: Address, format: AddressFormat = getAddressFormat(address.country)): string[] {\n  const text = (field: AddressFieldName) => {\n    const value = (address[field] ?? \"\").trim()\n    if (field !== \"region\" || !format.regions) return value\n    return format.regions.find((r) => r.code === value)?.name ?? value\n  }\n  return format.rows\n    .filter((row) => !row.includes(\"phone\"))\n    .map((row) => {\n      const parts = row.map((field) => ({ field, value: text(field) })).filter((p) => p.value)\n      return parts\n        .map((p, i) => {\n          if (i === 0) return p.value\n          const prev = parts[i - 1]\n          const tight = p.field === \"postalCode\" || prev.field === \"postalCode\" || prev.field === \"givenName\" || prev.field === \"familyName\"\n          return (tight ? \" \" : \", \") + p.value\n        })\n        .join(\"\")\n    })\n    .filter(Boolean)\n}\n",
      "type": "registry:lib",
      "target": "lib/address-format.ts"
    }
  ],
  "categories": [
    "ecommerce",
    "checkout"
  ],
  "type": "registry:block"
}