# Contact Form

> A complete contact form with name fields, phone with country selector, email, message textarea, and file attachment.

A shadcn/ui block from the [@manifest](https://ui.manifest.build) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/manifest/contact-form).

- Page, with a live preview: https://shadcn.exchange/r/manifest/contact-form
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/manifest/contact-form-light-640c0af91872bea2.png
- Registry item: https://ui.manifest.build/r/contact-form.json
- Categories: form
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://ui.manifest.build/r/contact-form.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `lucide-react`
- Registry items: `button`, `input`, `label`, `popover`

## Files

### `registry/form/contact-form.tsx`

````tsx
'use client';

import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { cn } from '@/lib/utils';
import { ChevronDown, Paperclip, Search, Send, X } from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
import { countries } from './countries';
import { demoContactFormData } from './demo/form';

/**
 * Data structure representing the contact form submission.
 * @interface ContactFormData
 * @property {string} firstName - The user's first name (required)
 * @property {string} lastName - The user's last name (required)
 * @property {string} [countryId] - The selected country identifier (e.g., 'us', 'fr')
 * @property {string} [countryCode] - The phone country code (e.g., '+1', '+33')
 * @property {string} [phoneNumber] - The user's phone number without country code
 * @property {string} [email] - The user's email address
 * @property {string} [message] - The message or project description
 * @property {File | null} [attachment] - An optional file attachment
 */
export interface ContactFormData {
  firstName?: string;
  lastName?: string;
  countryId?: string;
  countryCode?: string;
  phoneNumber?: string;
  email?: string;
  message?: string;
  attachment?: File | null;
}

/**
 * ═══════════════════════════════════════════════════════════════════════════
 * ContactFormProps
 * ═══════════════════════════════════════════════════════════════════════════
 *
 * Props for the ContactForm component with name fields, phone number with
 * country selector, email input, message textarea, and file attachment support.
 */
export interface ContactFormProps {
  data?: {
    /** The form title displayed at the top. */
    title?: string;
    /** Descriptive text below the title. */
    subtitle?: string;
    /** Custom label for the submit button. */
    submitLabel?: string;
    /** Pre-filled form values. */
    initialValues?: Partial<ContactFormData>;
  };
  actions?: {
    /** Called when the form is submitted with form data. */
    onSubmit?: (data: ContactFormData) => void;
  };
  appearance?: {
    /**
     * Whether to display the title section.
     * @default true
     */
    showTitle?: boolean;
  };
  control?: {
    /**
     * Shows loading state on submit button.
     * @default false
     */
    isLoading?: boolean;
  };
}


/**
 * A complete contact form component with name fields, phone number with country selector,
 * email input, message textarea, and file attachment support.
 *
 * Features:
 * - First and last name fields
 * - Phone number with searchable country code dropdown
 * - Email input with validation
 * - Message textarea for project descriptions
 * - File attachment with preview and removal
 * - Loading state support
 * - Customizable title and submit button text
 *
 * @component
 * @example
 * ```tsx
 * <ContactForm
 *   data={{
 *     title: "Get in Touch",
 *     subtitle: "We'd love to hear from you",
 *     submitLabel: "Send"
 *   }}
 *   actions={{
 *     onSubmit: (data) => console.log("Form submitted:", data)
 *   }}
 *   appearance={{ showTitle: true }}
 *   control={{ isLoading: false }}
 * />
 * ```
 */
export function ContactForm({ data, actions, appearance, control }: ContactFormProps) {
  const resolved: NonNullable<ContactFormProps['data']> = data ?? demoContactFormData
  const title = resolved.title
  const subtitle = resolved.subtitle
  const submitLabel = resolved.submitLabel ?? 'Submit'
  const initialValues = resolved.initialValues
  const { onSubmit } = actions ?? {};
  const { showTitle = true } = appearance ?? {};
  const { isLoading = false } = control ?? {};

  const fileInputRef = useRef<HTMLInputElement>(null);
  const searchInputRef = useRef<HTMLInputElement>(null);
  const [formData, setFormData] = useState<ContactFormData>({
    firstName: initialValues?.firstName ?? '',
    lastName: initialValues?.lastName ?? '',
    countryId: initialValues?.countryId ?? 'us',
    countryCode: initialValues?.countryCode ?? '+1',
    phoneNumber: initialValues?.phoneNumber ?? '',
    email: initialValues?.email ?? '',
    message: initialValues?.message ?? '',
    attachment: initialValues?.attachment ?? null,
  });
  const [countrySearch, setCountrySearch] = useState('');
  const [countryDropdownOpen, setCountryDropdownOpen] = useState(false);

  const selectedCountry = countries.find((c) => c.id === formData.countryId);

  const filteredCountries = countries.filter(
    (country) =>
      country.name.toLowerCase().includes(countrySearch.toLowerCase()) ||
      country.code.includes(countrySearch)
  );

  useEffect(() => {
    if (countryDropdownOpen && searchInputRef.current) {
      searchInputRef.current.focus();
    }
  }, [countryDropdownOpen]);

  const handleCountrySelect = (country: (typeof countries)[0]) => {
    setFormData((prev) => ({
      ...prev,
      countryId: country.id,
      countryCode: country.code,
    }));
    setCountryDropdownOpen(false);
    setCountrySearch('');
  };

  const handleChange = (field: keyof ContactFormData, value: string | File | null) => {
    setFormData((prev) => ({ ...prev, [field]: value }));
  };

  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0] || null;
    handleChange('attachment', file);
  };

  const handleRemoveFile = () => {
    handleChange('attachment', null);
    if (fileInputRef.current) {
      fileInputRef.current.value = '';
    }
  };

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    onSubmit?.(formData);
  };

  return (
    <div className="w-full bg-card rounded-xl p-6">
      {showTitle && (title || subtitle) && (
        <div className="mb-6">
          {title && <h2 className="text-xl font-semibold text-foreground">{title}</h2>}
          {subtitle && <p className="text-sm text-muted-foreground mt-1">{subtitle}</p>}
        </div>
      )}

      <form onSubmit={handleSubmit} className="space-y-4">
        {/* First Name & Last Name */}
        <div className="grid grid-cols-2 gap-4">
          <div className="space-y-2">
            <Label htmlFor="firstName">First Name</Label>
            <Input
              id="firstName"
              placeholder="John"
              value={formData.firstName}
              onChange={(e) => handleChange('firstName', e.target.value)}
              required
            />
          </div>
          <div className="space-y-2">
            <Label htmlFor="lastName">Last Name</Label>
            <Input
              id="lastName"
              placeholder="Doe"
              value={formData.lastName}
              onChange={(e) => handleChange('lastName', e.target.value)}
              required
            />
          </div>
        </div>

        {/* Phone Number & Email */}
        <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
          <div className="space-y-2">
            <Label htmlFor="phone">Phone Number</Label>
            <div className="flex gap-2">
              <Popover open={countryDropdownOpen} onOpenChange={setCountryDropdownOpen}>
                <PopoverTrigger asChild>
                  <button
                    type="button"
                    className={cn(
                      'flex items-center gap-1.5 h-9 px-3 rounded-lg border border-input bg-transparent text-sm transition-colors',
                      'hover:bg-muted focus-visible:border-foreground focus-visible:outline-none'
                    )}
                  >
                    {selectedCountry && (
                      <>
                        <span>{selectedCountry.flag}</span>
                        <span>{selectedCountry.code}</span>
                      </>
                    )}
                    <ChevronDown className="h-3.5 w-3.5 text-muted-foreground" />
                  </button>
                </PopoverTrigger>
                <PopoverContent className="w-[280px] p-0" align="start">
                  <div className="p-2 border-b border-border">
                    <div className="relative">
                      <Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
                      <input
                        ref={searchInputRef}
                        type="text"
                        placeholder="Search country..."
                        value={countrySearch}
                        onChange={(e) => setCountrySearch(e.target.value)}
                        className="w-full h-9 pl-9 pr-3 rounded-md border border-input bg-transparent text-sm placeholder:text-muted-foreground focus-visible:border-foreground focus-visible:outline-none"
                      />
                    </div>
                  </div>
                  <div className="max-h-[240px] overflow-y-auto p-1">
                    {filteredCountries.length === 0 ? (
                      <p className="text-sm text-muted-foreground text-center py-4">
                        No country found
                      </p>
                    ) : (
                      filteredCountries.map((country) => (
                        <button
                          key={country.id}
                          type="button"
                          onClick={() => handleCountrySelect(country)}
                          className={cn(
                            'w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-sm transition-colors text-left',
                            'hover:bg-muted',
                            formData.countryId === country.id && 'bg-muted'
                          )}
                        >
                          <span>{country.flag}</span>
                          <span className="flex-1">{country.name}</span>
                          <span className="text-muted-foreground">{country.code}</span>
                        </button>
                      ))
                    )}
                  </div>
                </PopoverContent>
              </Popover>
              <Input
                id="phone"
                type="tel"
                placeholder="(555) 123-4567"
                value={formData.phoneNumber}
                onChange={(e) => handleChange('phoneNumber', e.target.value)}
                className="flex-1"
              />
            </div>
          </div>

          <div className="space-y-2">
            <Label htmlFor="email">Email</Label>
            <Input
              id="email"
              type="email"
              placeholder="john@example.com"
              value={formData.email}
              onChange={(e) => handleChange('email', e.target.value)}
              required
            />
          </div>
        </div>

        {/* Message */}
        <div className="space-y-2">
          <Label htmlFor="message">Tell us about your project</Label>
          <textarea
            id="message"
            placeholder="Describe your project, goals, and timeline..."
            value={formData.message}
            onChange={(e) => handleChange('message', e.target.value)}
            rows={4}
            required
            className={cn(
              'border-input placeholder:text-muted-foreground flex w-full rounded-lg border bg-transparent px-3 py-2 text-base transition-colors outline-none md:text-sm resize-none',
              'focus-visible:border-foreground'
            )}
          />
        </div>

        {/* Actions */}
        <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
          <input
            ref={fileInputRef}
            type="file"
            onChange={handleFileChange}
            className="hidden"
            accept=".pdf,.doc,.docx,.txt,.png,.jpg,.jpeg"
          />

          {formData.attachment ? (
            <div className="flex items-center justify-center gap-2 px-3 py-2 bg-muted rounded-lg w-full sm:w-auto">
              <Paperclip className="h-4 w-4 text-muted-foreground flex-shrink-0" />
              <span className="text-sm text-foreground truncate max-w-[150px]">
                {formData.attachment.name}
              </span>
              <button
                type="button"
                onClick={handleRemoveFile}
                aria-label="Remove attachment"
                className="p-1 hover:bg-background rounded transition-colors"
              >
                <X className="h-4 w-4 text-muted-foreground" />
              </button>
            </div>
          ) : (
            <Button
              type="button"
              variant="outline"
              size="sm"
              onClick={() => fileInputRef.current?.click()}
              className="w-full sm:w-auto"
            >
              <Paperclip className="h-4 w-4 mr-2" />
              Attach a file
            </Button>
          )}

          <Button type="submit" size="sm" disabled={isLoading} className="w-full sm:w-auto">
            {isLoading ? (
              'Sending...'
            ) : (
              <>
                <Send className="h-4 w-4 mr-2" />
                {submitLabel}
              </>
            )}
          </Button>
        </div>
      </form>
    </div>
  );
}
````

### `registry/form/countries.ts`

```ts
export interface Country {
  id: string
  code: string
  name: string
  flag: string
}

export const countries: Country[] = [
  { id: 'af', code: '+93', name: 'Afghanistan', flag: '\u{1F1E6}\u{1F1EB}' },
  { id: 'al', code: '+355', name: 'Albania', flag: '\u{1F1E6}\u{1F1F1}' },
  { id: 'dz', code: '+213', name: 'Algeria', flag: '\u{1F1E9}\u{1F1FF}' },
  { id: 'ar', code: '+54', name: 'Argentina', flag: '\u{1F1E6}\u{1F1F7}' },
  { id: 'am', code: '+374', name: 'Armenia', flag: '\u{1F1E6}\u{1F1F2}' },
  { id: 'au', code: '+61', name: 'Australia', flag: '\u{1F1E6}\u{1F1FA}' },
  { id: 'at', code: '+43', name: 'Austria', flag: '\u{1F1E6}\u{1F1F9}' },
  { id: 'az', code: '+994', name: 'Azerbaijan', flag: '\u{1F1E6}\u{1F1FF}' },
  { id: 'bh', code: '+973', name: 'Bahrain', flag: '\u{1F1E7}\u{1F1ED}' },
  { id: 'bd', code: '+880', name: 'Bangladesh', flag: '\u{1F1E7}\u{1F1E9}' },
  { id: 'by', code: '+375', name: 'Belarus', flag: '\u{1F1E7}\u{1F1FE}' },
  { id: 'be', code: '+32', name: 'Belgium', flag: '\u{1F1E7}\u{1F1EA}' },
  { id: 'bo', code: '+591', name: 'Bolivia', flag: '\u{1F1E7}\u{1F1F4}' },
  { id: 'ba', code: '+387', name: 'Bosnia', flag: '\u{1F1E7}\u{1F1E6}' },
  { id: 'br', code: '+55', name: 'Brazil', flag: '\u{1F1E7}\u{1F1F7}' },
  { id: 'bg', code: '+359', name: 'Bulgaria', flag: '\u{1F1E7}\u{1F1EC}' },
  { id: 'kh', code: '+855', name: 'Cambodia', flag: '\u{1F1F0}\u{1F1ED}' },
  { id: 'cm', code: '+237', name: 'Cameroon', flag: '\u{1F1E8}\u{1F1F2}' },
  { id: 'ca', code: '+1', name: 'Canada', flag: '\u{1F1E8}\u{1F1E6}' },
  { id: 'cl', code: '+56', name: 'Chile', flag: '\u{1F1E8}\u{1F1F1}' },
  { id: 'cn', code: '+86', name: 'China', flag: '\u{1F1E8}\u{1F1F3}' },
  { id: 'co', code: '+57', name: 'Colombia', flag: '\u{1F1E8}\u{1F1F4}' },
  { id: 'cr', code: '+506', name: 'Costa Rica', flag: '\u{1F1E8}\u{1F1F7}' },
  { id: 'hr', code: '+385', name: 'Croatia', flag: '\u{1F1ED}\u{1F1F7}' },
  { id: 'cu', code: '+53', name: 'Cuba', flag: '\u{1F1E8}\u{1F1FA}' },
  { id: 'cy', code: '+357', name: 'Cyprus', flag: '\u{1F1E8}\u{1F1FE}' },
  { id: 'cz', code: '+420', name: 'Czech Republic', flag: '\u{1F1E8}\u{1F1FF}' },
  { id: 'dk', code: '+45', name: 'Denmark', flag: '\u{1F1E9}\u{1F1F0}' },
  { id: 'do', code: '+1', name: 'Dominican Republic', flag: '\u{1F1E9}\u{1F1F4}' },
  { id: 'ec', code: '+593', name: 'Ecuador', flag: '\u{1F1EA}\u{1F1E8}' },
  { id: 'eg', code: '+20', name: 'Egypt', flag: '\u{1F1EA}\u{1F1EC}' },
  { id: 'sv', code: '+503', name: 'El Salvador', flag: '\u{1F1F8}\u{1F1FB}' },
  { id: 'ee', code: '+372', name: 'Estonia', flag: '\u{1F1EA}\u{1F1EA}' },
  { id: 'et', code: '+251', name: 'Ethiopia', flag: '\u{1F1EA}\u{1F1F9}' },
  { id: 'fi', code: '+358', name: 'Finland', flag: '\u{1F1EB}\u{1F1EE}' },
  { id: 'fr', code: '+33', name: 'France', flag: '\u{1F1EB}\u{1F1F7}' },
  { id: 'ge', code: '+995', name: 'Georgia', flag: '\u{1F1EC}\u{1F1EA}' },
  { id: 'de', code: '+49', name: 'Germany', flag: '\u{1F1E9}\u{1F1EA}' },
  { id: 'gh', code: '+233', name: 'Ghana', flag: '\u{1F1EC}\u{1F1ED}' },
  { id: 'gr', code: '+30', name: 'Greece', flag: '\u{1F1EC}\u{1F1F7}' },
  { id: 'gt', code: '+502', name: 'Guatemala', flag: '\u{1F1EC}\u{1F1F9}' },
  { id: 'hn', code: '+504', name: 'Honduras', flag: '\u{1F1ED}\u{1F1F3}' },
  { id: 'hk', code: '+852', name: 'Hong Kong', flag: '\u{1F1ED}\u{1F1F0}' },
  { id: 'hu', code: '+36', name: 'Hungary', flag: '\u{1F1ED}\u{1F1FA}' },
  { id: 'is', code: '+354', name: 'Iceland', flag: '\u{1F1EE}\u{1F1F8}' },
  { id: 'in', code: '+91', name: 'India', flag: '\u{1F1EE}\u{1F1F3}' },
  { id: 'id', code: '+62', name: 'Indonesia', flag: '\u{1F1EE}\u{1F1E9}' },
  { id: 'ir', code: '+98', name: 'Iran', flag: '\u{1F1EE}\u{1F1F7}' },
  { id: 'iq', code: '+964', name: 'Iraq', flag: '\u{1F1EE}\u{1F1F6}' },
  { id: 'ie', code: '+353', name: 'Ireland', flag: '\u{1F1EE}\u{1F1EA}' },
  { id: 'il', code: '+972', name: 'Israel', flag: '\u{1F1EE}\u{1F1F1}' },
  { id: 'it', code: '+39', name: 'Italy', flag: '\u{1F1EE}\u{1F1F9}' },
  { id: 'jm', code: '+1', name: 'Jamaica', flag: '\u{1F1EF}\u{1F1F2}' },
  { id: 'jp', code: '+81', name: 'Japan', flag: '\u{1F1EF}\u{1F1F5}' },
  { id: 'jo', code: '+962', name: 'Jordan', flag: '\u{1F1EF}\u{1F1F4}' },
  { id: 'kz', code: '+7', name: 'Kazakhstan', flag: '\u{1F1F0}\u{1F1FF}' },
  { id: 'ke', code: '+254', name: 'Kenya', flag: '\u{1F1F0}\u{1F1EA}' },
  { id: 'kw', code: '+965', name: 'Kuwait', flag: '\u{1F1F0}\u{1F1FC}' },
  { id: 'lv', code: '+371', name: 'Latvia', flag: '\u{1F1F1}\u{1F1FB}' },
  { id: 'lb', code: '+961', name: 'Lebanon', flag: '\u{1F1F1}\u{1F1E7}' },
  { id: 'lt', code: '+370', name: 'Lithuania', flag: '\u{1F1F1}\u{1F1F9}' },
  { id: 'lu', code: '+352', name: 'Luxembourg', flag: '\u{1F1F1}\u{1F1FA}' },
  { id: 'my', code: '+60', name: 'Malaysia', flag: '\u{1F1F2}\u{1F1FE}' },
  { id: 'mx', code: '+52', name: 'Mexico', flag: '\u{1F1F2}\u{1F1FD}' },
  { id: 'ma', code: '+212', name: 'Morocco', flag: '\u{1F1F2}\u{1F1E6}' },
  { id: 'mm', code: '+95', name: 'Myanmar', flag: '\u{1F1F2}\u{1F1F2}' },
  { id: 'np', code: '+977', name: 'Nepal', flag: '\u{1F1F3}\u{1F1F5}' },
  { id: 'nl', code: '+31', name: 'Netherlands', flag: '\u{1F1F3}\u{1F1F1}' },
  { id: 'nz', code: '+64', name: 'New Zealand', flag: '\u{1F1F3}\u{1F1FF}' },
  { id: 'ng', code: '+234', name: 'Nigeria', flag: '\u{1F1F3}\u{1F1EC}' },
  { id: 'no', code: '+47', name: 'Norway', flag: '\u{1F1F3}\u{1F1F4}' },
  { id: 'om', code: '+968', name: 'Oman', flag: '\u{1F1F4}\u{1F1F2}' },
  { id: 'pk', code: '+92', name: 'Pakistan', flag: '\u{1F1F5}\u{1F1F0}' },
  { id: 'pa', code: '+507', name: 'Panama', flag: '\u{1F1F5}\u{1F1E6}' },
  { id: 'py', code: '+595', name: 'Paraguay', flag: '\u{1F1F5}\u{1F1FE}' },
  { id: 'pe', code: '+51', name: 'Peru', flag: '\u{1F1F5}\u{1F1EA}' },
  { id: 'ph', code: '+63', name: 'Philippines', flag: '\u{1F1F5}\u{1F1ED}' },
  { id: 'pl', code: '+48', name: 'Poland', flag: '\u{1F1F5}\u{1F1F1}' },
  { id: 'pt', code: '+351', name: 'Portugal', flag: '\u{1F1F5}\u{1F1F9}' },
  { id: 'pr', code: '+1', name: 'Puerto Rico', flag: '\u{1F1F5}\u{1F1F7}' },
  { id: 'qa', code: '+974', name: 'Qatar', flag: '\u{1F1F6}\u{1F1E6}' },
  { id: 'ro', code: '+40', name: 'Romania', flag: '\u{1F1F7}\u{1F1F4}' },
  { id: 'ru', code: '+7', name: 'Russia', flag: '\u{1F1F7}\u{1F1FA}' },
  { id: 'sa', code: '+966', name: 'Saudi Arabia', flag: '\u{1F1F8}\u{1F1E6}' },
  { id: 'sn', code: '+221', name: 'Senegal', flag: '\u{1F1F8}\u{1F1F3}' },
  { id: 'rs', code: '+381', name: 'Serbia', flag: '\u{1F1F7}\u{1F1F8}' },
  { id: 'sg', code: '+65', name: 'Singapore', flag: '\u{1F1F8}\u{1F1EC}' },
  { id: 'sk', code: '+421', name: 'Slovakia', flag: '\u{1F1F8}\u{1F1F0}' },
  { id: 'si', code: '+386', name: 'Slovenia', flag: '\u{1F1F8}\u{1F1EE}' },
  { id: 'za', code: '+27', name: 'South Africa', flag: '\u{1F1FF}\u{1F1E6}' },
  { id: 'kr', code: '+82', name: 'South Korea', flag: '\u{1F1F0}\u{1F1F7}' },
  { id: 'es', code: '+34', name: 'Spain', flag: '\u{1F1EA}\u{1F1F8}' },
  { id: 'lk', code: '+94', name: 'Sri Lanka', flag: '\u{1F1F1}\u{1F1F0}' },
  { id: 'se', code: '+46', name: 'Sweden', flag: '\u{1F1F8}\u{1F1EA}' },
  { id: 'ch', code: '+41', name: 'Switzerland', flag: '\u{1F1E8}\u{1F1ED}' },
  { id: 'tw', code: '+886', name: 'Taiwan', flag: '\u{1F1F9}\u{1F1FC}' },
  { id: 'th', code: '+66', name: 'Thailand', flag: '\u{1F1F9}\u{1F1ED}' },
  { id: 'tn', code: '+216', name: 'Tunisia', flag: '\u{1F1F9}\u{1F1F3}' },
  { id: 'tr', code: '+90', name: 'Turkey', flag: '\u{1F1F9}\u{1F1F7}' },
  { id: 'ua', code: '+380', name: 'Ukraine', flag: '\u{1F1FA}\u{1F1E6}' },
  { id: 'ae', code: '+971', name: 'United Arab Emirates', flag: '\u{1F1E6}\u{1F1EA}' },
  { id: 'gb', code: '+44', name: 'United Kingdom', flag: '\u{1F1EC}\u{1F1E7}' },
  { id: 'us', code: '+1', name: 'United States', flag: '\u{1F1FA}\u{1F1F8}' },
  { id: 'uy', code: '+598', name: 'Uruguay', flag: '\u{1F1FA}\u{1F1FE}' },
  { id: 'uz', code: '+998', name: 'Uzbekistan', flag: '\u{1F1FA}\u{1F1FF}' },
  { id: 've', code: '+58', name: 'Venezuela', flag: '\u{1F1FB}\u{1F1EA}' },
  { id: 'vn', code: '+84', name: 'Vietnam', flag: '\u{1F1FB}\u{1F1F3}' },
  { id: 'ye', code: '+967', name: 'Yemen', flag: '\u{1F1FE}\u{1F1EA}' },
  { id: 'zm', code: '+260', name: 'Zambia', flag: '\u{1F1FF}\u{1F1F2}' },
  { id: 'zw', code: '+263', name: 'Zimbabwe', flag: '\u{1F1FF}\u{1F1FC}' },
]
```

### `registry/form/demo/form.ts`

```ts
// Demo data for Form category components
// This file contains sample data used for component previews and documentation

/**
 * Generate available dates dynamically so the DateTimePicker preview
 * always has clickable dates regardless of when it is viewed.
 */
function generateAvailableDates(): Date[] {
  const dates: Date[] = [];
  const now = new Date();
  // Walk through current and next month, only keeping weekdays (Mon-Fri)
  for (let m = 0; m <= 1; m++) {
    for (let d = 1; d <= 28; d += 2) {
      const date = new Date(now.getFullYear(), now.getMonth() + m, d);
      const day = date.getDay();
      if (day !== 0 && day !== 6) {
        dates.push(date);
      }
    }
  }
  return dates;
}

export const demoContactFormData = {
  title: 'Get in Touch',
  subtitle: "We'd love to hear from you. Fill out the form below.",
  submitLabel: 'Send Message',
}

export const demoIssueReportFormData = {
  title: 'Report an Issue',
  teams: ['Engineering', 'Product', 'Design', 'Marketing', 'Operations'],
  locations: ['New York - HQ', 'San Francisco', 'London', 'Remote'],
  categories: {
    Software: ['Business App', 'Email', 'VPN', 'Browser', 'OS'],
    Hardware: ['Computer', 'Monitor', 'Keyboard', 'Mouse', 'Printer'],
    Network: ['Wi-Fi', 'Ethernet', 'VPN Access'],
    Access: ['Account', 'Permissions', 'Password Reset'],
  } as Record<string, string[]>,
  impacts: [
    { value: 'critical', label: 'Critical - Work stopped' },
    { value: 'high', label: 'High - Major feature broken' },
    { value: 'medium', label: 'Medium - Workaround available' },
    { value: 'low', label: 'Low - Minor inconvenience' },
  ],
  urgencies: [
    { value: 'immediate', label: 'Immediate' },
    { value: 'today', label: 'Today' },
    { value: 'this-week', label: 'This week' },
    { value: 'no-rush', label: 'No rush' },
  ],
  frequencies: [
    { value: 'constant', label: 'Constant' },
    { value: 'frequent', label: 'Frequent' },
    { value: 'occasional', label: 'Occasional' },
    { value: 'once', label: 'Happened once' },
  ],
  attemptedActions: [
    'Restarted the application',
    'Cleared browser cache',
    'Restarted computer',
    'Checked internet connection',
    'Contacted a colleague',
  ],
}

export const demoDateTimePickerData = {
  title: 'Select a Date & Time',
  availableDates: generateAvailableDates(),
  availableTimeSlots: [
    '9:00am',
    '10:00am',
    '11:30am',
    '1:00pm',
    '2:30pm',
    '4:00pm',
  ],
  timezone: 'Eastern Time - US & Canada',
};
```
