---
title: CriteriaTransports
description: A transport selection panel that lets users opt in to Club Med transportation and pick a departure airport from grouped, scrollable lists.
---

```tsx
'use client';
import { useState } from 'react';

import { CriteriaTransports } from '@/ui/criteria/CriteriaTransports';
import type { CriteriaTransportsProps } from '@/ui/criteria/CriteriaTransports';

const labels: CriteriaTransportsProps['labels'] = {
  toggleLabel: 'Add transportation from',
  topCitiesLabel: 'Top cities',
  otherCitiesLabel: 'Other cities',
  clubMedTransportTitle: 'Opt for Club Med transportation:',
  clubMedTransportDescription:
    'Assistance and support in case of unforeseen events. Comfort guaranteed from departure to arrival.',
  reset: 'Reset',
  apply: 'Continue',
};

const topCities = [
  { code: 'BOS', name: 'Boston Logan International (BOS)' },
  { code: 'NYC', name: 'New York all airports (NYC)' },
  { code: 'MIA', name: 'Miami International (MIA)' },
  { code: 'LAX', name: 'Los Angeles International (LAX)' },
  { code: 'SFO', name: 'San Francisco International (SFO)' },
];

const otherCities = [
  { code: 'ABQ', name: 'Albuquerque (ABQ)' },
  { code: 'ANC', name: 'Anchorage International (ANC)' },
  { code: 'ATL', name: 'Atlanta Hartsfield-Jackson (ATL)' },
  { code: 'AUS', name: 'Austin-Bergstrom International (AUS)' },
  { code: 'BNA', name: 'Nashville International (BNA)' },
  { code: 'BUF', name: 'Buffalo Niagara International (BUF)' },
  { code: 'CLT', name: 'Charlotte Douglas International (CLT)' },
  { code: 'CMH', name: 'John Glenn Columbus International (CMH)' },
  { code: 'DAL', name: 'Dallas Love Field (DAL)' },
  { code: 'DEN', name: 'Denver International (DEN)' },
  { code: 'DTW', name: 'Detroit Metropolitan Wayne County (DTW)' },
  { code: 'HNL', name: 'Daniel K. Inouye International (HNL)' },
  { code: 'IAD', name: 'Washington Dulles International (IAD)' },
  { code: 'IND', name: 'Indianapolis International (IND)' },
  { code: 'JAX', name: 'Jacksonville International (JAX)' },
  { code: 'LAS', name: 'Harry Reid International (LAS)' },
  { code: 'MCI', name: 'Kansas City International (MCI)' },
  { code: 'MCO', name: 'Orlando International (MCO)' },
  { code: 'MDW', name: 'Chicago Midway International (MDW)' },
  { code: 'MEM', name: 'Memphis International (MEM)' },
  { code: 'MSP', name: 'Minneapolis-Saint Paul International (MSP)' },
  { code: 'MSY', name: 'Louis Armstrong New Orleans International (MSY)' },
  { code: 'OAK', name: 'Oakland Metropolitan International (OAK)' },
  { code: 'ORD', name: "Chicago O'Hare International (ORD)" },
  { code: 'PHL', name: 'Philadelphia International (PHL)' },
  { code: 'PHX', name: 'Phoenix Sky Harbor International (PHX)' },
  { code: 'PIT', name: 'Pittsburgh International (PIT)' },
  { code: 'PDX', name: 'Portland International (PDX)' },
  { code: 'RDU', name: 'Raleigh-Durham International (RDU)' },
  { code: 'RSW', name: 'Southwest Florida International (RSW)' },
  { code: 'SAN', name: 'San Diego International (SAN)' },
  { code: 'SAT', name: 'San Antonio International (SAT)' },
  { code: 'SEA', name: 'Seattle-Tacoma International (SEA)' },
  { code: 'SJC', name: 'Norman Y. Mineta San José International (SJC)' },
  { code: 'SLC', name: 'Salt Lake City International (SLC)' },
  { code: 'SMF', name: 'Sacramento International (SMF)' },
  { code: 'SNA', name: 'John Wayne Airport Orange County (SNA)' },
  { code: 'STL', name: 'St. Louis Lambert International (STL)' },
  { code: 'TPA', name: 'Tampa International (TPA)' },
  { code: 'TUL', name: 'Tulsa International (TUL)' },
];

export function CriteriaTransportsDemo() {
  const [enabled, setEnabled] = useState(false);
  const [selected, setSelected] = useState<string | null>(null);

  return (
    <div className="w-[380px] space-y-24">
      <CriteriaTransports
        labels={labels}
        title="Transport"
        enabled={enabled}
        topCities={topCities}
        otherCities={otherCities}
        selectedAirportCode={selected}
        onToggle={setEnabled}
        onSelect={setSelected}
        onReset={() => {
          setEnabled(false);
          setSelected(null);
        }}
        onApply={() => {}}
      />
      {selected && (
        <p className="text-b4">
          Selected: <strong>{selected}</strong>
        </p>
      )}
    </div>
  );
}
```

## Installation

Using the CLI:

```bash
npx shadcn@latest add https://develop.trident-ui.pro.clubmed/r/criteria-transports.json
```

Or manually, copy the following code into `/ui/criteria/CriteriaTransports.tsx`:

```tsx
'use client';
import clsx from 'clsx';
import React, { forwardRef, type FunctionComponent, useId, useRef } from 'react';

import { Icon } from '@clubmed/trident-icons';
import { Button } from '../buttons/Button';
import { Switch } from '../forms/Switch';
import { ElasticHeight } from '../ElasticHeight';
import { Radio } from '../forms/radios/Radio';
import { RadioSelect } from '../forms/RadioSelect';
import type { RadioSelectOption } from '../forms/RadioSelect';
import { TextField } from '../forms/TextField';

export interface Airport {
  code: string;
  name: string;
}

export interface CriteriaTransportsLabels {
  toggleLabel: string;
  topCitiesLabel: string;
  otherCitiesLabel: string;
  clubMedTransportTitle: string;
  clubMedTransportDescription: string;
  reset?: string;
  apply?: string;
}

export interface CriteriaTransportsB2BLabels {
  roundTripLabel: string;
  oneWayLabel: string;
  cabinClassLabel: string;
  departureDateLabel: string;
}

export interface CriteriaTransportsProps {
  labels: CriteriaTransportsLabels;
  title?: string;
  /** card (default): white outer card with sand inner. flat: no background, fields rendered directly */
  variant?: 'card' | 'flat';
  /** Switches between B2C (default) and B2B mode. B2B mode adds flight type, cabin class, and departure selectors. */
  mode?: 'b2c' | 'b2b';
  enabled: boolean;
  topCities: Airport[];
  otherCities: Airport[];
  selectedAirportCode?: string | null;
  className?: string;
  onToggle: (enabled: boolean) => void;
  onSelect: (code: string | null) => void;
  onReset?: () => void;
  onApply?: () => void;
  // B2B-specific props (ignored in b2c mode)
  b2bLabels?: CriteriaTransportsB2BLabels;
  /** Currently selected flight type (B2B mode). Defaults to 'roundtrip'. */
  flightType?: 'roundtrip' | 'oneway';
  onFlightTypeChange?: (type: 'roundtrip' | 'oneway') => void;
  /** Currently selected cabin class value (B2B mode). */
  cabinClass?: string;
  /** Options for the cabin class RadioSelect (B2B mode). */
  cabinClassOptions?: RadioSelectOption<string>[];
  onCabinClassChange?: (event: Event, value: string) => void;
  /** Controlled value for the departure airport TextField; also used to filter the airport list. */
  departureQuery?: string;
  onDepartureQueryChange?: (event: Event, value: string) => void;
  /** Error message shown in B2B mode when a prerequisite (e.g. resort selection) is missing. */
  error?: string;
}

const AirportItem = forwardRef<
  HTMLDivElement,
  {
    airport: Airport;
    selected: boolean;
    onSelect: (code: string | null) => void;
    tabIndex?: number;
    onKeyDown?: (e: React.KeyboardEvent<HTMLDivElement>) => void;
    small?: boolean;
  }
>(({ airport, selected, onSelect, tabIndex = 0, onKeyDown, small = true }, ref) => (
  <div
    ref={ref}
    role="option"
    aria-selected={selected}
    tabIndex={tabIndex}
    className={clsx(
      'flex w-full cursor-pointer justify-between gap-12 text-left bg-white text-black transition-all aria-selected:invert',
      small ? 'text-b4 px-8 py-4 rounded-8 items-center' : 'text-b3 p-12 rounded-16 items-start',
    )}
    onClick={() => onSelect(selected ? null : airport.code)}
    onKeyDown={(e) => {
      if (e.key === 'Enter' || e.key === ' ') {
        e.preventDefault();
        onSelect(selected ? null : airport.code);
      }
      onKeyDown?.(e);
    }}
  >
    <span className="font-normal">{airport.name}</span>
    <span
      className={clsx(
        'text-inherit shrink-0 transition-all duration-300 in-[[aria-selected=true]]:opacity-100 in-[[aria-selected=false]]:opacity-0',
      )}
    >
      <Icon aria-hidden name="CheckDefault" width="24px" className="shrink-0" />
    </span>
  </div>
));

const AirportGroup: FunctionComponent<{
  label: string;
  airports: Airport[];
  selectedCode?: string | null;
  onSelect: (code: string | null) => void;
}> = ({ label, airports, selectedCode, onSelect }) => {
  const labelId = useId();
  const itemRefs = useRef<(HTMLDivElement | null)[]>([]);
  const selectedIndex = airports.findIndex((a) => a.code === selectedCode);
  const tabbableIndex = selectedIndex >= 0 ? selectedIndex : 0;

  const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>, index: number) => {
    let next = index;
    if (e.key === 'ArrowDown') next = Math.min(index + 1, airports.length - 1);
    else if (e.key === 'ArrowUp') next = Math.max(index - 1, 0);
    else return;
    e.preventDefault();
    itemRefs.current[next]?.focus();
  };

  return (
    <div className="flex flex-col gap-4">
      <div className="border-lightGrey border-b px-1 py-12">
        <p id={labelId} className="text-b3 font-semibold">
          {label}
        </p>
      </div>
      <div role="listbox" aria-labelledby={labelId} className="flex flex-col gap-4">
        {airports.map((airport, index) => (
          <AirportItem
            key={airport.code}
            ref={(el) => {
              itemRefs.current[index] = el;
            }}
            airport={airport}
            selected={selectedCode === airport.code}
            onSelect={onSelect}
            tabIndex={index === tabbableIndex ? 0 : -1}
            onKeyDown={(e) => handleKeyDown(e, index)}
          />
        ))}
      </div>
    </div>
  );
};

const TransportCard: FunctionComponent<{ title: string; description: string }> = ({
  title,
  description,
}) => (
  <div className="rounded-16 border-lightGrey flex flex-col gap-20 border bg-white p-20 mb-20">
    <div className="flex items-center gap-20">
      <div className="bg-lightSand rounded-full flex shrink-0 items-center justify-center p-12">
        <Icon aria-hidden name="GlobePlane" width="24px" />
      </div>
      <p className="text-b3 font-semibold">{title}</p>
    </div>
    <p className="text-b3 font-normal">{description}</p>
  </div>
);

const B2BFlightTypeSelector: FunctionComponent<{
  roundTripLabel: string;
  oneWayLabel: string;
  value?: 'roundtrip' | 'oneway';
  onChange?: (type: 'roundtrip' | 'oneway') => void;
}> = ({ roundTripLabel, oneWayLabel, value = 'roundtrip', onChange }) => {
  const groupName = useId();
  return (
    <div role="radiogroup" className="flex gap-20">
      <Radio
        name={groupName}
        value="roundtrip"
        checked={value === 'roundtrip'}
        onChange={() => onChange?.('roundtrip')}
      >
        <span className="text-b3 font-normal">{roundTripLabel}</span>
      </Radio>
      <Radio
        name={groupName}
        value="oneway"
        checked={value === 'oneway'}
        onChange={() => onChange?.('oneway')}
      >
        <span className="text-b3 font-normal">{oneWayLabel}</span>
      </Radio>
    </div>
  );
};

const B2BErrorCard: FunctionComponent<{ message: string }> = ({ message }) => (
  <div className="rounded-16 border-lightGrey flex border bg-white p-20 mb-20">
    <div className="flex items-center gap-20">
      <div className="bg-lightSand rounded-full flex shrink-0 items-center justify-center p-12">
        <Icon aria-hidden name="Error" width="24px" className="text-sienna" />
      </div>
      <p className="text-b3 font-semibold text-sienna">{message}</p>
    </div>
  </div>
);

export const CriteriaTransports: FunctionComponent<CriteriaTransportsProps> = ({
  labels,
  title,
  variant = 'card',
  mode = 'b2c',
  enabled,
  topCities,
  otherCities,
  selectedAirportCode,
  className,
  onToggle,
  onSelect,
  onReset,
  onApply,
  b2bLabels,
  flightType = 'roundtrip',
  onFlightTypeChange,
  cabinClass,
  cabinClassOptions,
  onCabinClassChange,
  departureQuery,
  onDepartureQueryChange,
  error,
}) => {
  const isCard = variant === 'card';
  const isB2B = mode === 'b2b';
  const switchLabelId = useId();

  const header = (
    <div className="flex flex-col gap-8">
      {title && <p className="text-b2 font-semibold">{title}</p>}
      <div className="flex items-center justify-between">
        <p id={switchLabelId} className="text-b3 font-semibold">
          {labels.toggleLabel}
        </p>
        <Switch
          checked={enabled}
          onChange={(_, val) => onToggle(val)}
          aria-labelledby={switchLabelId}
        />
      </div>
    </div>
  );

  const airportList = (
    <div className="relative" style={{ timelineScope: '--airport-list' } as React.CSSProperties}>
      <div
        aria-hidden
        className="absolute top-0 left-0 right-0 pointer-events-none z-10"
        style={
          {
            height: '20px',
            background: 'linear-gradient(to bottom, rgba(246,239,231,1), rgba(246,239,231,0))',
            animationName: 'criteria-transports-top-blur',
            animationTimeline: '--airport-list',
            animationRange: '0px 40px',
            animationFillMode: 'both',
            animationTimingFunction: 'linear',
            opacity: 0,
          } as React.CSSProperties
        }
      />
      <div
        role="region"
        aria-label={labels.topCitiesLabel || labels.otherCitiesLabel}
        tabIndex={0}
        className="flex max-h-[420px] flex-col gap-20 pb-20 overflow-y-auto"
        style={{ scrollTimelineName: '--airport-list' } as React.CSSProperties}
      >
        {topCities.length > 0 && (
          <AirportGroup
            label={labels.topCitiesLabel}
            airports={topCities}
            selectedCode={selectedAirportCode}
            onSelect={onSelect}
          />
        )}
        {otherCities.length > 0 && (
          <AirportGroup
            label={labels.otherCitiesLabel}
            airports={otherCities}
            selectedCode={selectedAirportCode}
            onSelect={onSelect}
          />
        )}
      </div>
    </div>
  );

  const filteredTopCities =
    isB2B && departureQuery
      ? topCities.filter((a) => a.name.toLowerCase().includes(departureQuery.toLowerCase()))
      : topCities;

  const filteredOtherCities =
    isB2B && departureQuery
      ? otherCities.filter((a) => a.name.toLowerCase().includes(departureQuery.toLowerCase()))
      : otherCities;

  const b2bAirportList = (
    <div className="relative" style={{ timelineScope: '--airport-list' } as React.CSSProperties}>
      <div
        aria-hidden
        className="absolute top-0 left-0 right-0 pointer-events-none z-10"
        style={
          {
            height: '20px',
            background: 'linear-gradient(to bottom, rgba(246,239,231,1), rgba(246,239,231,0))',
            animationName: 'criteria-transports-top-blur',
            animationTimeline: '--airport-list',
            animationRange: '0px 40px',
            animationFillMode: 'both',
            animationTimingFunction: 'linear',
            opacity: 0,
          } as React.CSSProperties
        }
      />
      <div
        role="region"
        aria-label={labels.topCitiesLabel || labels.otherCitiesLabel}
        tabIndex={0}
        className="flex max-h-[420px] flex-col gap-20 pb-20 overflow-y-auto"
        style={{ scrollTimelineName: '--airport-list' } as React.CSSProperties}
      >
        {filteredTopCities.length > 0 && (
          <AirportGroup
            label={labels.topCitiesLabel}
            airports={filteredTopCities}
            selectedCode={selectedAirportCode}
            onSelect={onSelect}
          />
        )}
        {filteredOtherCities.length > 0 && (
          <AirportGroup
            label={labels.otherCitiesLabel}
            airports={filteredOtherCities}
            selectedCode={selectedAirportCode}
            onSelect={onSelect}
          />
        )}
      </div>
    </div>
  );

  const b2bExpandedContent =
    isB2B && b2bLabels ? (
      error ? (
        <B2BErrorCard message={error} />
      ) : (
        <div className="flex flex-col gap-8">
          <B2BFlightTypeSelector
            roundTripLabel={b2bLabels.roundTripLabel}
            oneWayLabel={b2bLabels.oneWayLabel}
            value={flightType}
            onChange={onFlightTypeChange}
          />
          <RadioSelect
            label={b2bLabels.cabinClassLabel}
            value={cabinClass}
            options={cabinClassOptions}
            onChange={onCabinClassChange}
          />
          <TextField
            label={b2bLabels.departureDateLabel}
            value={departureQuery}
            icon="Search"
            onChange={onDepartureQueryChange}
          />
          {b2bAirportList}
        </div>
      )
    ) : null;

  const content = (
    <>
      {header}
      <div>
        <ElasticHeight isExpanded={enabled}>{b2bExpandedContent ?? airportList}</ElasticHeight>
        <ElasticHeight isExpanded={!enabled}>
          <TransportCard
            title={labels.clubMedTransportTitle}
            description={labels.clubMedTransportDescription}
          />
        </ElasticHeight>
      </div>
    </>
  );

  const footer = (onReset || onApply) && (
    <div className="flex items-center justify-between">
      {onReset && labels.reset ? (
        <button
          className="text-b3 cursor-pointer font-semibold underline"
          onClick={onReset}
          type="button"
        >
          {labels.reset}
        </button>
      ) : (
        <span />
      )}
      {onApply && labels.apply && (
        <Button color="saffron" onClick={onApply}>
          {labels.apply}
        </Button>
      )}
    </div>
  );

  const keyframes = (
    <style>{`
      @keyframes criteria-transports-top-blur {
        from { opacity: 0; }
        to   { opacity: 1; }
      }
    `}</style>
  );

  if (!isCard) {
    return (
      <>
        <div className={clsx('flex flex-col gap-20', className)}>
          {content}
          {footer}
        </div>
        {keyframes}
      </>
    );
  }

  return (
    <>
      <div
        className={clsx(
          'rounded-16 border-lightGrey flex flex-col gap-20 border bg-white p-20',
          className,
        )}
      >
        <div className="rounded-16 bg-lightSand flex flex-col gap-20 px-20 pt-20">{content}</div>
        {footer}
      </div>
      {keyframes}
    </>
  );
};
```

## Usage

```tsx showLineNumbers
import { CriteriaTransports } from '@/ui/criteria/CriteriaTransports';

export function Component() {
  const [enabled, setEnabled] = useState(false);
  const [selected, setSelected] = useState<string | null>(null);

  return (
    <CriteriaTransports
      labels={{
        toggleLabel: 'Add transportation from',
        topCitiesLabel: 'Top cities',
        otherCitiesLabel: 'Other cities',
        clubMedTransportTitle: 'Opt for Club Med transportation:',
        clubMedTransportDescription:
          'Assistance and support in case of unforeseen events.',
        reset: 'Reset',
        apply: 'Continue',
      }}
      title="Transport"
      enabled={enabled}
      topCities={[
        { code: 'BOS', name: 'Boston Logan International (BOS)' },
        { code: 'NYC', name: 'New York all airports (NYC)' },
      ]}
      otherCities={[
        { code: 'ABQ', name: 'Albuquerque (ABQ)' },
      ]}
      selectedAirportCode={selected}
      onToggle={setEnabled}
      onSelect={setSelected}
      onReset={() => { setEnabled(false); setSelected(null); }}
      onApply={() => {}}
    />
  );
}
```

## B2B Mode

The component supports a B2B booking flow via the `mode="b2b"` prop. In this mode, additional flight configuration fields (flight type, cabin class, and departure) appear above the airport list when transport is enabled. A single `error` prop covers the case where a prerequisite — such as resort selection — has not been met.

```tsx
'use client';
import { useState } from 'react';

import { CriteriaTransports } from '@/ui/criteria/CriteriaTransports';
import type {
  CriteriaTransportsProps,
  CriteriaTransportsB2BLabels,
} from '@/ui/criteria/CriteriaTransports';
import type { RadioSelectOption } from '@/ui/forms/RadioSelect';

const labels: CriteriaTransportsProps['labels'] = {
  toggleLabel: 'Add transportation from',
  topCitiesLabel: 'Top cities',
  otherCitiesLabel: 'Other cities',
  clubMedTransportTitle: 'Opt for Club Med transportation:',
  clubMedTransportDescription:
    'Assistance and support in case of unforeseen events. Comfort guaranteed from departure to arrival.',
  reset: 'Reset',
  apply: 'Confirm',
};

const b2bLabels: CriteriaTransportsB2BLabels = {
  roundTripLabel: 'Round trip',
  oneWayLabel: 'One way',
  cabinClassLabel: 'Class',
  departureDateLabel: 'From',
};

const cabinClassOptions: RadioSelectOption<string>[] = [
  { value: 'all', label: 'All classes' },
  { value: 'economy', label: 'Economy' },
  { value: 'business', label: 'Business' },
  { value: 'first', label: 'First class' },
];

const topCities = [
  { code: 'BOS', name: 'Boston Logan International (BOS)' },
  { code: 'NYC', name: 'New York all airports (NYC)' },
  { code: 'MIA', name: 'Miami International (MIA)' },
];

const otherCities = [
  { code: 'ABQ', name: 'Albuquerque (ABQ)' },
  { code: 'ANC', name: 'Anchorage International (ANC)' },
  { code: 'ATL', name: 'Atlanta Hartsfield-Jackson (ATL)' },
  { code: 'AUS', name: 'Austin-Bergstrom International (AUS)' },
  { code: 'DEN', name: 'Denver International (DEN)' },
  { code: 'LAX', name: 'Los Angeles International (LAX)' },
  { code: 'SEA', name: 'Seattle-Tacoma International (SEA)' },
  { code: 'SFO', name: 'San Francisco International (SFO)' },
];

export function CriteriaTransportsB2BDemo() {
  const [enabled, setEnabled] = useState(false);
  const [selected, setSelected] = useState<string | null>(null);
  const [flightType, setFlightType] = useState<'roundtrip' | 'oneway'>('roundtrip');
  const [cabinClass, setCabinClass] = useState<string | undefined>(undefined);
  const [departureQuery, setDepartureQuery] = useState('');

  return (
    <div className="w-[380px] space-y-24">
      <CriteriaTransports
        mode="b2b"
        labels={labels}
        b2bLabels={b2bLabels}
        title="Transport"
        enabled={enabled}
        topCities={topCities}
        otherCities={otherCities}
        selectedAirportCode={selected}
        flightType={flightType}
        cabinClass={cabinClass}
        cabinClassOptions={cabinClassOptions}
        departureQuery={departureQuery}
        onToggle={setEnabled}
        onSelect={setSelected}
        onFlightTypeChange={setFlightType}
        onCabinClassChange={(_event, value) => setCabinClass(value)}
        onDepartureQueryChange={(_event, value) => setDepartureQuery(value)}
        onReset={() => {
          setEnabled(false);
          setSelected(null);
          setFlightType('roundtrip');
          setCabinClass(undefined);
          setDepartureQuery('');
        }}
        onApply={() => {}}
      />
      {selected && (
        <p className="text-b4">
          Selected: <strong>{selected}</strong>
        </p>
      )}
    </div>
  );
}
```

### Error state

Pass an `error` string to display an error card instead of the B2B form. This is used when a prerequisite (e.g. selecting a resort) has not been completed.

```tsx
<CriteriaTransports
  mode="b2b"
  enabled
  error="Please select a resort before choosing transport."
  {/* ...other required props */}
/>
```

## API Reference

### CriteriaTransports

| Prop | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| `labels` | `CriteriaTransportsLabels` | — | Required. Localized strings for all visible text. |
| `title` | `string` | — | Optional section heading rendered above the toggle. |
| `variant` | `'card' \| 'flat'` | `'card'` | `card` wraps content in a white bordered card with a sand inner panel. `flat` renders fields without any card background. |
| `mode` | `'b2c' \| 'b2b'` | `'b2c'` | Switches between B2C and B2B layouts. B2B adds flight type, cabin class, and departure selectors above the airport list. |
| `enabled` | `boolean` | — | Controls whether transport selection is active (toggle on/off). |
| `topCities` | `Airport[]` | — | Promoted airports shown under the "Top cities" heading. |
| `otherCities` | `Airport[]` | — | Full airport list shown under the "Other cities" heading. |
| `selectedAirportCode` | `string \| null` | — | Code of the currently selected airport, or `null` for no selection. |
| `className` | `string` | — | Extra CSS classes applied to the root element. |
| `onToggle` | `(enabled: boolean) => void` | — | Called when the user flips the transport toggle. |
| `onSelect` | `(code: string \| null) => void` | — | Called with the airport code on selection, or `null` when deselecting. |
| `onReset` | `() => void` | — | When provided, renders a "Reset" link in the footer. |
| `onApply` | `() => void` | — | When provided, renders a "Continue/Confirm" button in the footer. |
| `b2bLabels` | `CriteriaTransportsB2BLabels` | — | Required in B2B mode. Localized strings for the B2B-specific fields. |
| `flightType` | `'roundtrip' \| 'oneway'` | `'roundtrip'` | Currently selected flight type (B2B mode). |
| `onFlightTypeChange` | `(type: 'roundtrip' \| 'oneway') => void` | — | Called when the user changes the flight type radio (B2B mode). |
| `cabinClass` | `string` | — | Currently selected cabin class value. Passed to the `RadioSelect` trigger (B2B mode). |
| `cabinClassOptions` | `RadioSelectOption<string>[]` | — | Options list for the cabin class `RadioSelect` dropdown (B2B mode). |
| `onCabinClassChange` | `(event: Event, value: string) => void` | — | Called when the user picks a cabin class from the dropdown (B2B mode). |
| `departureQuery` | `string` | — | Controlled value for the departure `TextField`; also filters the airport list below (B2B mode). |
| `onDepartureQueryChange` | `(event: Event, value: string) => void` | — | Called as the user types in the departure field (B2B mode). |
| `error` | `string` | — | Error message shown in B2B mode instead of the flight form when a prerequisite is missing. |

### Airport

| Field | Type | Description |
| ----- | ---- | ----------- |
| `code` | `string` | IATA airport code (e.g. `"BOS"`). Used as the selection key. |
| `name` | `string` | Full display label (e.g. `"Boston Logan International (BOS)"`). |

### CriteriaTransportsLabels

| Field | Type | Description |
| ----- | ---- | ----------- |
| `toggleLabel` | `string` | Label next to the on/off switch (e.g. "Add transportation from"). |
| `topCitiesLabel` | `string` | Group heading for promoted airports. |
| `otherCitiesLabel` | `string` | Group heading for remaining airports. |
| `clubMedTransportTitle` | `string` | Title inside the Club Med transport info card (toggle off state). |
| `clubMedTransportDescription` | `string` | Body text inside the Club Med transport info card. |
| `reset` | `string` | Label for the reset link. Required when `onReset` is provided. |
| `apply` | `string` | Label for the continue/confirm button. Required when `onApply` is provided. |

### CriteriaTransportsB2BLabels

| Field | Type | Description |
| ----- | ---- | ----------- |
| `roundTripLabel` | `string` | Label for the round-trip radio option (e.g. "Round trip"). |
| `oneWayLabel` | `string` | Label for the one-way radio option (e.g. "One way"). |
| `cabinClassLabel` | `string` | Field label for the cabin class selector (e.g. "Class"). |
| `departureDateLabel` | `string` | Field label for the departure date/city field (e.g. "From"). |

## Behaviour

- **Toggle off** — Displays a Club Med transport info card explaining the benefits of booking transport. No airport selection is available (same in both B2C and B2B modes).
- **Toggle on (B2C)** — Hides the info card and reveals a scrollable airport list grouped into "Top cities" and "Other cities". Either group is omitted when its array is empty.
- **Toggle on (B2B, no error)** — Shows flight type radio buttons, a cabin class selector, a departure date/city field, then the airport list below.
- **Toggle on (B2B, with error)** — Shows an error card with the `error` message instead of the B2B form. Use this when a prerequisite step (e.g. resort selection) must be completed first.
- **Flight type (B2B)** — Radio group (using the `Radio` component) allowing single selection between round-trip and one-way. Fully controlled via `flightType` and `onFlightTypeChange`.
- **Cabin class (B2B)** — Uses the `RadioSelect` component. Pass `cabinClassOptions` to populate the dropdown and `cabinClass` for the controlled value. `onCabinClassChange` fires `(event, value)` on selection.
- **Departure field (B2B)** — Uses the `TextField` component with a calendar icon. `departureQuery` controls the input value; `onDepartureQueryChange` fires `(event, value)` on every keystroke. The same query also filters the airport list shown below the field.
- **Airport selection** — Single select: clicking an already-selected airport calls `onSelect(null)` to deselect it. The selected item is styled with a black background and white text.
- **Scroll** — The airport list container is capped at `max-h-[420px]` with `overflow-y-auto`, so long lists scroll without expanding the card.
- **Footer** — Rendered only when at least one of `onReset` or `onApply` is provided.

## Accessibility

- The toggle uses `role="switch"` via the `Switch` component.
- The flight type selector uses a native `<input type="radio">` group with visible labels, ensuring full keyboard and screen reader support.
- Each airport item carries `role="option"` and `aria-selected` to communicate selection state.
- The cabin class selector uses `RadioSelect`, which renders a `<button aria-haspopup="listbox">` trigger and a `role="listbox"` dropdown with radio options — fully keyboard navigable.
- The departure field uses `TextField`, which renders a standard `<input>` with a visible label and calendar icon — supports all native keyboard interactions.
- The continue and reset buttons are native `<button>` elements with visible labels.
