{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "map-carousel",
  "version": "2.1.2",
  "category": "map",
  "meta": {
    "preview": "https://ui.manifest.build/previews/map-carousel.png",
    "version": "2.1.2",
    "changelog": {
      "1.0.0": "Initial release with interactive map and location cards",
      "1.1.0": "Made image and price fields optional in Location interface",
      "1.1.2": "Added comprehensive JSDoc documentation",
      "1.2.0": "Moved to map category for better organization",
      "1.2.1": "Removed Next.js dependency - now uses React-only lazy loading for map components",
      "1.2.2": "Updated Props interface with decorative header and sub-parameter JSDoc documentation",
      "1.2.3": "Fixed defensive property access to handle empty objects and null values",
      "1.3.0": "Added display modes (inline/fullscreen) with split-screen layout, filters, and ChatGPT host integration",
      "1.4.0": "Made filters fully configurable via data prop with custom filter sections and match functions",
      "2.0.0": "BREAKING: Removed onExpand and onFiltersApply actions. Expand and filter apply are now internal.",
      "2.0.1": "Removed default content data - component only renders explicitly provided data",
      "2.0.2": "Migrated from OpenAI Apps SDK to MCP Apps protocol for host communication",
      "2.0.3": "Fixed Leaflet CSS injection deduplication and hardcoded date display",
      "2.0.4": "Adjusted demo map zoom level to show more of the San Francisco bay area",
      "2.1.0": "Added demo data defaults - component renders demo content when no data prop is provided",
      "2.1.1": "Fixed react-leaflet Invalid hook call errors by using React.lazy instead of useEffect dynamic imports",
      "2.1.2": "Fixed Invalid hook call errors by replacing react-leaflet with vanilla leaflet API"
    }
  },
  "changelog": {
    "1.0.0": "Initial release with interactive map and location cards",
    "1.1.0": "Made image and price fields optional in Location interface",
    "1.1.2": "Added comprehensive JSDoc documentation",
    "1.2.0": "Moved to map category for better organization",
    "1.2.1": "Removed Next.js dependency - now uses React-only lazy loading for map components",
    "1.2.2": "Updated Props interface with decorative header and sub-parameter JSDoc documentation",
    "1.2.3": "Fixed defensive property access to handle empty objects and null values",
    "1.3.0": "Added display modes (inline/fullscreen) with split-screen layout, filters, and ChatGPT host integration",
    "1.4.0": "Made filters fully configurable via data prop with custom filter sections and match functions",
    "2.0.0": "BREAKING: Removed onExpand and onFiltersApply actions. Expand and filter apply are now internal.",
    "2.0.1": "Removed default content data - component only renders explicitly provided data",
    "2.0.2": "Migrated from OpenAI Apps SDK to MCP Apps protocol for host communication",
    "2.0.3": "Fixed Leaflet CSS injection deduplication and hardcoded date display",
    "2.0.4": "Adjusted demo map zoom level to show more of the San Francisco bay area",
    "2.1.0": "Added demo data defaults - component renders demo content when no data prop is provided",
    "2.1.1": "Fixed react-leaflet Invalid hook call errors by using React.lazy instead of useEffect dynamic imports",
    "2.1.2": "Fixed Invalid hook call errors by replacing react-leaflet with vanilla leaflet API"
  },
  "title": "Map Carousel",
  "author": "MNFST, Inc",
  "description": "Interactive map with location markers and a draggable carousel of cards.",
  "dependencies": [
    "lucide-react",
    "leaflet"
  ],
  "devDependencies": [
    "@types/leaflet"
  ],
  "registryDependencies": [
    "button",
    "checkbox"
  ],
  "files": [
    {
      "path": "registry/map/map-carousel.tsx",
      "content": "'use client'\n\nimport { Button } from '@/components/ui/button'\nimport { Checkbox } from '@/components/ui/checkbox'\nimport { cn } from '@/lib/utils'\nimport { ChevronDown, MapPin, Maximize2, SlidersHorizontal, X } from 'lucide-react'\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport { demoMapLocations, demoMapCenter, demoMapZoom } from './demo/map'\n\n/**\n * Represents a location/hotel to display on the map.\n * @interface Location\n * @property {string} [name] - Location name\n * @property {string} [subtitle] - Subtitle (e.g., neighborhood)\n * @property {string} [image] - Location image URL\n * @property {number} [price] - Price value\n * @property {string} [priceLabel] - Full price text (e.g., \"$284 total Jan 29 - Feb 1\")\n * @property {string} [priceSubtext] - Additional price info (e.g., \"USD • Includes taxes\")\n * @property {number} [rating] - Rating value (e.g., 8.6)\n * @property {[number, number]} coordinates - Lat/lng coordinates\n * @property {string} [link] - External link URL\n */\nexport interface Location {\n  name?: string\n  subtitle?: string\n  image?: string\n  price?: number\n  priceLabel?: string\n  priceSubtext?: string\n  rating?: number\n  coordinates: [number, number] // [lat, lng]\n  link?: string\n}\n\n/**\n * Available map tile styles.\n * @typedef {\"voyager\" | \"voyager-smooth\" | \"positron\" | \"dark-matter\" | \"openstreetmap\"} MapStyle\n */\nexport type MapStyle =\n  | 'voyager'\n  | 'voyager-smooth'\n  | 'positron'\n  | 'dark-matter'\n  | 'openstreetmap'\n\n// Filter configuration for fullscreen variant\n/**\n * Configuration for a single filter section.\n * @interface FilterSectionConfig\n */\nexport interface FilterSectionConfig {\n  /** Unique identifier for this filter (used in filter state). */\n  id: string\n  /** Display title for the filter section. */\n  title: string\n  /** Available options for this filter. */\n  options: string[]\n  /**\n   * Function to check if a location matches the selected filter values.\n   * @param location - The location to check\n   * @param selectedValues - Currently selected filter values\n   * @returns true if location matches, false otherwise\n   */\n  matchFn?: (location: Location, selectedValues: string[]) => boolean\n}\n\n/** State tracking selected values for each filter by id. */\nexport type FilterState = Record<string, string[]>\n\nconst createEmptyFilterState = (filters: FilterSectionConfig[]): FilterState => {\n  return filters.reduce((acc, filter) => {\n    acc[filter.id] = []\n    return acc\n  }, {} as FilterState)\n}\n\n/**\n * ═══════════════════════════════════════════════════════════════════════════\n * MapCarouselProps\n * ═══════════════════════════════════════════════════════════════════════════\n *\n * Props for configuring an interactive map with a horizontal carousel of\n * location cards. Clicking a marker or card selects that location.\n * Supports inline (map with carousel) and fullscreen (split-screen) modes.\n */\nexport interface MapCarouselProps {\n  data?: {\n    /** Array of locations to display on the map with price markers. */\n    locations?: Location[]\n    /**\n     * Map center coordinates as [latitude, longitude].\n     * @default [37.7899, -122.4034]\n     */\n    center?: [number, number]\n    /**\n     * Initial zoom level for the map.\n     * @default 14\n     */\n    zoom?: number\n    /**\n     * Map tile style (voyager, positron, dark-matter, etc.).\n     * @default \"voyager\"\n     */\n    mapStyle?: MapStyle\n    /** Optional title displayed above the list in fullscreen mode. */\n    title?: string\n    /**\n     * Filter sections configuration for fullscreen mode.\n     * Each filter section has an id, title, options, and optional matchFn.\n     * If not provided, no filters will be shown.\n     */\n    filters?: FilterSectionConfig[]\n  }\n  actions?: {\n    /** Called when a user selects a location via marker or card click. */\n    onSelectLocation?: (location: Location) => void\n  }\n  appearance?: {\n    /**\n     * Height of the map container (inline mode only).\n     * @default \"504px\"\n     */\n    mapHeight?: string\n    /**\n     * Display mode for the component.\n     * - inline: Map with carousel cards at bottom\n     * - pip: Same as inline (compact view)\n     * - fullscreen: Split-screen with cards on left, filters, map on right\n     * @default \"inline\"\n     */\n    displayMode?: 'inline' | 'pip' | 'fullscreen'\n  }\n}\n\n\n// Hotel card component\nfunction HotelCard({\n  location,\n  isSelected,\n  onClick\n}: {\n  location: Location\n  isSelected: boolean\n  onClick: () => void\n}) {\n  return (\n    <button\n      onClick={onClick}\n      className={cn(\n        'relative flex gap-3 p-2 rounded-xl border bg-card min-w-[300px] max-w-[300px] text-left transition-all shrink-0 cursor-pointer select-none shadow-[0_4px_20px_rgba(0,0,0,0.08)]',\n        isSelected\n          ? 'ring-1 ring-foreground border-foreground'\n          : 'hover:border-foreground/30'\n      )}\n    >\n      {/* Rating badge - top right */}\n      {location.rating && (\n        <div className=\"absolute top-2 right-2 bg-green-600 text-white text-[10px] font-bold rounded-md px-1.5 py-0.5\">\n          {location.rating}\n        </div>\n      )}\n\n      {/* Image */}\n      {location.image && (\n        <div className=\"relative shrink-0\">\n          <img\n            src={location.image}\n            alt={location.name || 'Location image'}\n            className=\"w-24 h-20 rounded-lg object-cover pointer-events-none\"\n            draggable={false}\n          />\n        </div>\n      )}\n\n      {/* Content */}\n      <div className=\"flex flex-col justify-center min-w-0 flex-1 pointer-events-none\">\n        {location.name && (\n          <h3 className=\"font-medium text-sm leading-tight truncate pr-8\">\n            {location.name}\n          </h3>\n        )}\n        {location.subtitle && (\n          <p className=\"text-xs text-muted-foreground truncate\">\n            {location.subtitle}\n          </p>\n        )}\n        <div className=\"mt-1.5\">\n          {location.price !== undefined && (\n            <p className=\"text-sm\">\n              {location.priceLabel ? (\n                <span className=\"font-semibold\">{location.priceLabel}</span>\n              ) : (\n                <span className=\"font-semibold\">${location.price} total</span>\n              )}\n            </p>\n          )}\n          {location.priceSubtext && (\n            <p className=\"text-[10px] text-muted-foreground\">\n              {location.priceSubtext}\n            </p>\n          )}\n        </div>\n      </div>\n    </button>\n  )\n}\n\n// Location card for fullscreen list view\nfunction LocationListCard({\n  location,\n  isSelected,\n  onClick,\n  onMouseEnter,\n  onMouseLeave\n}: {\n  location: Location\n  isSelected: boolean\n  onClick: () => void\n  onMouseEnter: () => void\n  onMouseLeave: () => void\n}) {\n  return (\n    <div\n      onClick={onClick}\n      onMouseEnter={onMouseEnter}\n      onMouseLeave={onMouseLeave}\n      className={cn(\n        'flex gap-3 p-3 border-b transition-colors cursor-pointer',\n        isSelected && 'bg-accent'\n      )}\n    >\n      {/* Thumbnail */}\n      {location.image && (\n        <div className=\"h-20 w-20 flex-shrink-0 overflow-hidden rounded-md bg-muted\">\n          <img\n            src={location.image}\n            alt={location.name || 'Location image'}\n            className=\"h-full w-full object-cover\"\n          />\n        </div>\n      )}\n      {/* Location Info */}\n      <div className=\"flex-1 min-w-0\">\n        {location.price !== undefined && (\n          <p className=\"font-semibold text-sm\">${location.price} total</p>\n        )}\n        {location.priceSubtext && (\n          <p className=\"text-xs text-muted-foreground\">{location.priceSubtext}</p>\n        )}\n        {location.name && (\n          <p className=\"text-sm font-medium mt-1 line-clamp-1\">{location.name}</p>\n        )}\n        {location.subtitle && (\n          <p className=\"text-xs text-muted-foreground mt-0.5 line-clamp-1\">\n            {location.subtitle}\n          </p>\n        )}\n        {location.rating && (\n          <div className=\"flex items-center gap-1 mt-1\">\n            <span className=\"bg-green-600 text-white text-[10px] font-bold rounded-md px-1.5 py-0.5\">\n              {location.rating}\n            </span>\n          </div>\n        )}\n      </div>\n    </div>\n  )\n}\n\n// Filter section component with expandable checkbox list\nfunction FilterSection({\n  title,\n  options,\n  selected,\n  onChange,\n  defaultExpanded = true,\n  showLimit = 5\n}: {\n  title: string\n  options: string[]\n  selected: string[]\n  onChange: (values: string[]) => void\n  defaultExpanded?: boolean\n  showLimit?: number\n}) {\n  const [expanded, setExpanded] = useState(defaultExpanded)\n  const [showAll, setShowAll] = useState(false)\n\n  const visibleOptions = showAll ? options : options.slice(0, showLimit)\n  const hasMore = options.length > showLimit\n\n  const toggleOption = (option: string) => {\n    if (selected.includes(option)) {\n      onChange(selected.filter(s => s !== option))\n    } else {\n      onChange([...selected, option])\n    }\n  }\n\n  return (\n    <div className=\"border-b border-border/50\">\n      <button\n        onClick={() => setExpanded(!expanded)}\n        className=\"flex w-full items-center justify-between py-4 text-sm font-medium hover:text-foreground/80 transition-colors\"\n      >\n        <span>{title}</span>\n        <ChevronDown className={cn(\n          \"h-4 w-4 text-muted-foreground transition-transform duration-200\",\n          expanded && \"rotate-180\"\n        )} />\n      </button>\n      <div className={cn(\n        \"grid transition-all duration-200 ease-out\",\n        expanded ? \"grid-rows-[1fr] opacity-100\" : \"grid-rows-[0fr] opacity-0\"\n      )}>\n        <div className=\"overflow-hidden\">\n          <div className=\"space-y-2 pb-4\">\n            {visibleOptions.map(option => (\n              <label\n                key={option}\n                className=\"flex items-center gap-3 cursor-pointer group\"\n              >\n                <Checkbox\n                  checked={selected.includes(option)}\n                  onCheckedChange={() => toggleOption(option)}\n                  className=\"h-4 w-4\"\n                />\n                <span className=\"text-sm text-muted-foreground group-hover:text-foreground transition-colors\">\n                  {option}\n                </span>\n              </label>\n            ))}\n            {hasMore && (\n              <button\n                onClick={() => setShowAll(!showAll)}\n                className=\"mt-1 text-xs text-primary hover:text-primary/80 transition-colors\"\n              >\n                {showAll ? 'Show less' : `View ${options.length - showLimit} more`}\n              </button>\n            )}\n          </div>\n        </div>\n      </div>\n    </div>\n  )\n}\n\n// Filter panel that slides over the location list\nfunction FilterPanel({\n  isOpen,\n  onClose,\n  filterConfigs,\n  filterState,\n  onFiltersChange,\n  onApply,\n  onReset,\n  resultCount\n}: {\n  isOpen: boolean\n  onClose: () => void\n  filterConfigs: FilterSectionConfig[]\n  filterState: FilterState\n  onFiltersChange: (filters: FilterState) => void\n  onApply: () => void\n  onReset: () => void\n  resultCount: number\n}) {\n  const activeFiltersCount = Object.values(filterState).flat().length\n\n  return (\n    <>\n      {/* Backdrop */}\n      <div\n        className={cn(\n          \"absolute inset-0 bg-background/60 backdrop-blur-[2px] transition-opacity duration-300 z-10\",\n          isOpen ? \"opacity-100\" : \"opacity-0 pointer-events-none\"\n        )}\n        onClick={onClose}\n      />\n\n      {/* Panel */}\n      <div\n        className={cn(\n          \"absolute inset-0 bg-background z-20 flex flex-col transition-all duration-300 ease-out\",\n          isOpen\n            ? \"opacity-100 translate-x-0\"\n            : \"opacity-0 -translate-x-4 pointer-events-none\"\n        )}\n      >\n        {/* Header */}\n        <div className=\"flex items-center justify-between border-b px-4 py-3\">\n          <div className=\"flex items-center gap-2\">\n            <span className=\"font-semibold\">Filters</span>\n            {activeFiltersCount > 0 && (\n              <span className=\"bg-primary text-primary-foreground text-xs px-2 py-0.5 rounded-full\">\n                {activeFiltersCount}\n              </span>\n            )}\n          </div>\n          <Button\n            variant=\"ghost\"\n            size=\"icon\"\n            className=\"h-8 w-8\"\n            onClick={onClose}\n            aria-label=\"Close filters\"\n          >\n            <X className=\"h-4 w-4\" />\n          </Button>\n        </div>\n\n        {/* Filter sections - dynamically rendered */}\n        <div className=\"flex-1 overflow-y-auto px-4 py-3 space-y-1\">\n          {filterConfigs.map((config) => (\n            <FilterSection\n              key={config.id}\n              title={config.title}\n              options={config.options}\n              selected={filterState[config.id] || []}\n              onChange={(values) => onFiltersChange({ ...filterState, [config.id]: values })}\n            />\n          ))}\n        </div>\n\n        {/* Footer with actions */}\n        <div className=\"border-t px-4 py-3 space-y-2\">\n          <Button\n            className=\"w-full\"\n            onClick={onApply}\n          >\n            Show {resultCount} locations\n          </Button>\n          {activeFiltersCount > 0 && (\n            <Button\n              variant=\"ghost\"\n              className=\"w-full text-muted-foreground hover:text-foreground\"\n              onClick={onReset}\n            >\n              Reset all filters\n            </Button>\n          )}\n        </div>\n      </div>\n    </>\n  )\n}\n\n// Map placeholder shown during SSR or when Leaflet isn't loaded\nfunction MapPlaceholder({ height }: { height?: string }) {\n  return (\n    <div\n      className=\"bg-muted/30 flex items-center justify-center\"\n      style={{ height: height || '100%' }}\n    >\n      <div className=\"flex flex-col items-center gap-2 text-muted-foreground\">\n        <MapPin className=\"h-8 w-8\" />\n        <span className=\"text-sm\">Loading map...</span>\n      </div>\n    </div>\n  )\n}\n\n// Vanilla Leaflet map – bypasses react-leaflet entirely to avoid dual-React hook errors.\n// Uses the leaflet JS API directly via refs so only the consumer's React copy exists.\ninterface LeafletMapConfig {\n  center: [number, number]\n  zoom: number\n  tileConfig: { url: string; attribution: string }\n  locations: Location[]\n  selectedIndex: number | null\n  onSelectLocation: (location: Location, index: number) => void\n  style?: React.CSSProperties\n}\n\nfunction VanillaLeafletMap({\n  center,\n  zoom,\n  tileConfig,\n  locations,\n  selectedIndex,\n  onSelectLocation,\n  style\n}: LeafletMapConfig) {\n  const containerRef = useRef<HTMLDivElement>(null)\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  const mapInstanceRef = useRef<any>(null)\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  const leafletRef = useRef<any>(null)\n  // eslint-disable-next-line @typescript-eslint/no-explicit-any\n  const markersRef = useRef<any[]>([])\n  const callbackRef = useRef(onSelectLocation)\n  const [ready, setReady] = useState(false)\n\n  callbackRef.current = onSelectLocation\n\n  // Initialize the Leaflet map once on mount\n  useEffect(() => {\n    if (!containerRef.current || mapInstanceRef.current) return\n    let cancelled = false\n\n    ;(async () => {\n      const L = (await import('leaflet')).default\n      if (cancelled || !containerRef.current) return\n\n      const LEAFLET_CSS_ID = 'leaflet-css-1.9.4'\n      if (!document.getElementById(LEAFLET_CSS_ID)) {\n        const link = document.createElement('link')\n        link.id = LEAFLET_CSS_ID\n        link.rel = 'stylesheet'\n        link.href = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css'\n        document.head.appendChild(link)\n      }\n\n      const map = L.map(containerRef.current, {\n        center,\n        zoom,\n        zoomControl: true,\n        scrollWheelZoom: true\n      })\n      L.tileLayer(tileConfig.url, { attribution: tileConfig.attribution }).addTo(map)\n\n      leafletRef.current = L\n      mapInstanceRef.current = map\n      setReady(true)\n    })()\n\n    return () => {\n      cancelled = true\n      mapInstanceRef.current?.remove()\n      mapInstanceRef.current = null\n      leafletRef.current = null\n    }\n  }, []) // eslint-disable-line react-hooks/exhaustive-deps\n\n  // Sync markers whenever locations or selection change\n  useEffect(() => {\n    const L = leafletRef.current\n    const map = mapInstanceRef.current\n    if (!L || !map) return\n\n    markersRef.current.forEach((m: { remove: () => void }) => m.remove())\n    markersRef.current = []\n\n    locations.forEach((location, index) => {\n      const isSelected = selectedIndex === index\n      const icon = L.divIcon({\n        className: '',\n        html: `<div style=\"\n          position: absolute; left: 50%; top: 50%;\n          transform: translate(-50%, -50%);\n          display: inline-block; padding: 4px 8px; border-radius: 8px;\n          font-size: 12px; font-weight: 600;\n          font-family: system-ui, -apple-system, sans-serif;\n          white-space: nowrap;\n          box-shadow: 0 2px 8px rgba(0,0,0,0.15), 0 1px 3px rgba(0,0,0,0.1);\n          z-index: ${isSelected ? '1000' : '1'};\n          ${isSelected ? 'background-color: #18181b; color: white;' : 'background-color: white; color: #18181b;'}\n        \">${location.price !== undefined ? `$${location.price}` : location.name ?? 'Location'}</div>`,\n        iconSize: [60, 24],\n        iconAnchor: [30, 12]\n      })\n\n      const marker = L.marker(location.coordinates, {\n        icon,\n        zIndexOffset: isSelected ? 1000 : 0\n      })\n      marker.on('click', () => callbackRef.current(location, index))\n      marker.addTo(map)\n      markersRef.current.push(marker)\n    })\n  }, [locations, selectedIndex, ready])\n\n  return (\n    <div className=\"relative\" style={style ?? { height: '100%', width: '100%' }}>\n      <div ref={containerRef} style={{ height: '100%', width: '100%' }} />\n      {!ready && (\n        <div className=\"absolute inset-0\">\n          <MapPlaceholder />\n        </div>\n      )}\n    </div>\n  )\n}\n\n/**\n * Gets the tile configuration for a given map style.\n * @param {MapStyle} style - The map style to use\n * @returns {{ url: string; attribution: string }} The tile URL and attribution\n */\nconst getTileConfig = (style: MapStyle) => {\n  const configs: Record<MapStyle, { url: string; attribution: string }> = {\n    // Voyager - Colorful, detailed, Apple Maps-like (recommended default)\n    voyager: {\n      url: 'https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png',\n      attribution:\n        '&copy; <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap</a> &copy; <a href=\"https://carto.com/attributions\">CARTO</a>'\n    },\n    // Voyager with labels under roads - cleaner look\n    'voyager-smooth': {\n      url: 'https://{s}.basemaps.cartocdn.com/rastertiles/voyager_labels_under/{z}/{x}/{y}{r}.png',\n      attribution:\n        '&copy; <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap</a> &copy; <a href=\"https://carto.com/attributions\">CARTO</a>'\n    },\n    // Positron - Light, minimal, clean\n    positron: {\n      url: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png',\n      attribution:\n        '&copy; <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap</a> &copy; <a href=\"https://carto.com/attributions\">CARTO</a>'\n    },\n    // Dark Matter - Dark theme\n    'dark-matter': {\n      url: 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png',\n      attribution:\n        '&copy; <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap</a> &copy; <a href=\"https://carto.com/attributions\">CARTO</a>'\n    },\n    // OpenStreetMap - Standard, detailed\n    openstreetmap: {\n      url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',\n      attribution:\n        '&copy; <a href=\"https://www.openstreetmap.org/copyright\">OpenStreetMap</a> contributors'\n    }\n  }\n  return configs[style]\n}\n\n/**\n * An interactive map with a horizontal carousel of location cards.\n * Clicking a marker or card selects that location and syncs the view.\n *\n * Features:\n * - Leaflet map with multiple tile style options\n * - Price markers on map locations\n * - Inline mode: Map with draggable carousel at bottom\n * - Fullscreen mode: Split-screen with cards on left, filters, map on right\n * - Location cards with image, rating, and price\n * - Selection sync between map and carousel/list\n * - MCP Apps display mode integration\n *\n * @component\n * @example\n * ```tsx\n * <MapCarousel\n *   data={{\n *     locations: [\n *       {\n *         name: \"Hotel Carlton\",\n *         subtitle: \"Downtown\",\n *         image: \"/hotel.jpg\",\n *         price: 284,\n *         rating: 8.6,\n *         coordinates: [37.7879, -122.4137]\n *       }\n *     ],\n *     center: [37.7899, -122.4034],\n *     zoom: 14,\n *     mapStyle: \"voyager\",\n *     title: \"Hotels in San Francisco\"\n *   }}\n *   actions={{\n *     onSelectLocation: (loc) => console.log(\"Selected:\", loc.name),\n *     onExpand: () => console.log(\"Expand to fullscreen\")\n *   }}\n *   appearance={{\n *     mapHeight: \"504px\",\n *     displayMode: \"inline\"\n *   }}\n * />\n * ```\n */\nexport function MapCarousel({ data, actions, appearance }: MapCarouselProps) {\n  const resolvedData: NonNullable<MapCarouselProps['data']> = data ?? { locations: demoMapLocations, center: demoMapCenter, zoom: demoMapZoom }\n  const {\n    locations = [],\n    center = [37.7899, -122.4034], // San Francisco\n    zoom = 14,\n    mapStyle = 'voyager',\n    title,\n    filters: filterConfigs = []\n  } = resolvedData\n\n  const tileConfig = getTileConfig(mapStyle)\n  const { onSelectLocation } = actions ?? {}\n  const { mapHeight = '504px' } = appearance ?? {}\n\n  const displayMode = appearance?.displayMode ?? 'inline'\n\n  const [selectedIndex, setSelectedIndex] = useState<number | null>(null)\n  const [isDragging, setIsDragging] = useState(false)\n  const [startX, setStartX] = useState(0)\n  const [scrollLeft, setScrollLeft] = useState(0)\n  const [hasDragged, setHasDragged] = useState(false)\n  const carouselRef = useRef<HTMLDivElement>(null)\n  const cardRefs = useRef<Map<number, HTMLButtonElement>>(new Map())\n\n  // Filter state for fullscreen mode - initialized from filter configs\n  const emptyFilterState = createEmptyFilterState(filterConfigs)\n  const [showFilters, setShowFilters] = useState(false)\n  const [filterState, setFilterState] = useState<FilterState>(emptyFilterState)\n  const [appliedFilterState, setAppliedFilterState] = useState<FilterState>(emptyFilterState)\n\n  // Refs for fullscreen scroll functionality\n  const listContainerRef = useRef<HTMLDivElement>(null)\n  const locationItemRefs = useRef<Map<number, HTMLDivElement>>(new Map())\n\n  // Filter locations based on applied filters using dynamic matchFn\n  const filterLocations = useCallback((locationsToFilter: Location[], filtersToApply: FilterState): Location[] => {\n    // If no filter configs, return all locations\n    if (filterConfigs.length === 0) return locationsToFilter\n\n    return locationsToFilter.filter(location => {\n      // Check each filter section\n      for (const config of filterConfigs) {\n        const selectedValues = filtersToApply[config.id] || []\n        // Skip if no values selected for this filter\n        if (selectedValues.length === 0) continue\n\n        // Use the matchFn if provided, otherwise skip this filter\n        if (config.matchFn) {\n          if (!config.matchFn(location, selectedValues)) {\n            return false\n          }\n        }\n      }\n      return true\n    })\n  }, [filterConfigs])\n\n  // Scroll to location in list when selected from map\n  const scrollToLocation = useCallback((locationIndex: number) => {\n    const locationElement = locationItemRefs.current.get(locationIndex)\n    if (locationElement && listContainerRef.current) {\n      const container = listContainerRef.current\n      const elementTop = locationElement.offsetTop\n      const elementHeight = locationElement.offsetHeight\n      const containerHeight = container.offsetHeight\n      const scrollTo = elementTop - containerHeight / 2 + elementHeight / 2\n\n      container.scrollTo({\n        top: scrollTo,\n        behavior: 'smooth'\n      })\n    }\n  }, [])\n\n  // Handle location selection\n  const handleSelectLocation = useCallback(\n    (location: Location, index: number) => {\n      setSelectedIndex(index)\n      onSelectLocation?.(location)\n\n      // Scroll to the selected card (inline mode)\n      const cardElement = cardRefs.current.get(index)\n      if (cardElement && carouselRef.current) {\n        const container = carouselRef.current\n        const cardLeft = cardElement.offsetLeft\n        const cardWidth = cardElement.offsetWidth\n        const containerWidth = container.offsetWidth\n        const scrollTo = cardLeft - containerWidth / 2 + cardWidth / 2\n\n        container.scrollTo({\n          left: scrollTo,\n          behavior: 'smooth'\n        })\n      }\n    },\n    [onSelectLocation]\n  )\n\n  // Handle expand button click — display mode changes handled by host wrapper\n  const handleExpand = () => {\n    // No-op: display mode is managed by the HostAPIProvider\n  }\n\n  // Drag handlers for carousel\n  const handleMouseDown = useCallback((e: React.MouseEvent) => {\n    if (!carouselRef.current) return\n    setIsDragging(true)\n    setHasDragged(false)\n    setStartX(e.pageX - carouselRef.current.offsetLeft)\n    setScrollLeft(carouselRef.current.scrollLeft)\n  }, [])\n\n  const handleMouseMove = useCallback(\n    (e: React.MouseEvent) => {\n      if (!isDragging || !carouselRef.current) return\n      e.preventDefault()\n      const x = e.pageX - carouselRef.current.offsetLeft\n      const walk = (x - startX) * 1.5\n      if (Math.abs(walk) > 5) {\n        setHasDragged(true)\n      }\n      carouselRef.current.scrollLeft = scrollLeft - walk\n    },\n    [isDragging, startX, scrollLeft]\n  )\n\n  const handleMouseUp = useCallback(() => {\n    setIsDragging(false)\n  }, [])\n\n  const handleMouseLeave = useCallback(() => {\n    setIsDragging(false)\n  }, [])\n\n  // Touch handlers for mobile\n  const handleTouchStart = useCallback((e: React.TouchEvent) => {\n    if (!carouselRef.current) return\n    setIsDragging(true)\n    setHasDragged(false)\n    setStartX(e.touches[0].pageX - carouselRef.current.offsetLeft)\n    setScrollLeft(carouselRef.current.scrollLeft)\n  }, [])\n\n  const handleTouchMove = useCallback(\n    (e: React.TouchEvent) => {\n      if (!isDragging || !carouselRef.current) return\n      const x = e.touches[0].pageX - carouselRef.current.offsetLeft\n      const walk = (x - startX) * 1.5\n      if (Math.abs(walk) > 5) {\n        setHasDragged(true)\n      }\n      carouselRef.current.scrollLeft = scrollLeft - walk\n    },\n    [isDragging, startX, scrollLeft]\n  )\n\n  const handleTouchEnd = useCallback(() => {\n    setIsDragging(false)\n  }, [])\n\n  // Handle card click (only if not dragging)\n  const handleCardClick = useCallback(\n    (location: Location, index: number) => {\n      if (hasDragged) return\n      handleSelectLocation(location, index)\n      if (location.link) {\n        window.open(location.link, '_blank', 'noopener,noreferrer')\n      }\n    },\n    [hasDragged, handleSelectLocation]\n  )\n\n  // Fullscreen mode - split-screen with cards on left, map on right\n  if (displayMode === 'fullscreen') {\n    const handleLocationHover = (locationIndex: number | null) => {\n      setSelectedIndex(locationIndex)\n    }\n\n    const handleLocationClick = (location: Location, index: number) => {\n      setSelectedIndex(index)\n      onSelectLocation?.(location)\n      if (location.link) {\n        window.open(location.link, '_blank', 'noopener,noreferrer')\n      }\n    }\n\n    const handleMapMarkerClick = (location: Location, index: number) => {\n      setSelectedIndex(index)\n      scrollToLocation(index)\n      onSelectLocation?.(location)\n    }\n\n    const handleFilterButtonClick = () => {\n      setFilterState(appliedFilterState)\n      setShowFilters(true)\n    }\n\n    const handleApplyFilters = () => {\n      setAppliedFilterState(filterState)\n      setShowFilters(false)\n    }\n\n    const handleResetFilters = () => {\n      setFilterState(emptyFilterState)\n      setAppliedFilterState(emptyFilterState)\n    }\n\n    // Get filtered locations\n    const filteredLocations = filterLocations(locations, appliedFilterState)\n    // Get preview count for filter panel\n    const previewFilteredCount = filterLocations(locations, filterState).length\n    // Count of active filters\n    const activeFiltersCount = Object.values(appliedFilterState).flat().length\n    // Check if filters are configured\n    const hasFilters = filterConfigs.length > 0\n\n    return (\n      <div className=\"flex w-full h-full min-h-[600px] bg-background\">\n        {/* Left Panel - Location List */}\n        <div className=\"w-[380px] flex-shrink-0 border-r flex flex-col relative\">\n          {/* Header */}\n          <div className=\"flex items-center justify-between gap-3 border-b px-4 py-3\">\n            <div className=\"flex items-center gap-2 min-w-0\">\n              {title && <span className=\"font-semibold truncate\">{title}</span>}\n              <span className=\"text-muted-foreground text-xs whitespace-nowrap\">| {filteredLocations.length}</span>\n            </div>\n            {hasFilters && (\n              <Button\n                variant=\"outline\"\n                size=\"sm\"\n                className=\"gap-2 flex-shrink-0\"\n                onClick={handleFilterButtonClick}\n              >\n                <SlidersHorizontal className=\"h-4 w-4\" />\n                <span className=\"hidden sm:inline\">Filters</span>\n                {activeFiltersCount > 0 && (\n                  <span className=\"bg-primary text-primary-foreground text-xs px-1.5 py-0.5 rounded-full\">\n                    {activeFiltersCount}\n                  </span>\n                )}\n              </Button>\n            )}\n          </div>\n\n          {/* Scrollable Location List */}\n          <div ref={listContainerRef} className=\"flex-1 overflow-y-auto\">\n            {filteredLocations.length === 0 ? (\n              <div className=\"flex flex-col items-center justify-center h-full p-8 text-center\">\n                <p className=\"text-muted-foreground\">No locations match your filters</p>\n                <Button\n                  variant=\"link\"\n                  className=\"mt-2\"\n                  onClick={handleResetFilters}\n                >\n                  Reset filters\n                </Button>\n              </div>\n            ) : (\n              filteredLocations.map((location, index) => (\n                <div\n                  key={index}\n                  ref={(el) => {\n                    if (el) locationItemRefs.current.set(index, el)\n                  }}\n                >\n                  <LocationListCard\n                    location={location}\n                    isSelected={selectedIndex === index}\n                    onClick={() => handleLocationClick(location, index)}\n                    onMouseEnter={() => handleLocationHover(index)}\n                    onMouseLeave={() => handleLocationHover(null)}\n                  />\n                </div>\n              ))\n            )}\n          </div>\n\n          {/* Filter Panel Overlay */}\n          {hasFilters && (\n            <FilterPanel\n              isOpen={showFilters}\n              onClose={() => setShowFilters(false)}\n              filterConfigs={filterConfigs}\n              filterState={filterState}\n              onFiltersChange={setFilterState}\n              onApply={handleApplyFilters}\n              onReset={handleResetFilters}\n              resultCount={previewFilteredCount}\n            />\n          )}\n        </div>\n\n        {/* Right Panel - Map */}\n        <div className=\"flex flex-1 min-w-0 relative\">\n          <VanillaLeafletMap\n            center={center}\n            zoom={zoom}\n            tileConfig={tileConfig}\n            locations={filteredLocations}\n            selectedIndex={selectedIndex}\n            onSelectLocation={handleMapMarkerClick}\n          />\n        </div>\n      </div>\n    )\n  }\n\n  // Inline and PiP modes - Map with carousel at bottom\n  return (\n    <div\n      className=\"relative w-full rounded-xl border bg-card overflow-hidden\"\n      style={{ height: mapHeight }}\n    >\n      {/* Expand button in top right */}\n      <div className=\"absolute top-3 right-3 z-[1001]\">\n        <Button\n          variant=\"secondary\"\n          size=\"icon\"\n          className=\"h-8 w-8 bg-background/90 backdrop-blur-sm shadow-md\"\n          onClick={handleExpand}\n          aria-label=\"Expand to fullscreen\"\n        >\n          <Maximize2 className=\"h-4 w-4\" />\n        </Button>\n      </div>\n\n      {/* Map Section - Full size */}\n      <VanillaLeafletMap\n        center={center}\n        zoom={zoom}\n        tileConfig={tileConfig}\n        locations={locations}\n        selectedIndex={selectedIndex}\n        onSelectLocation={handleSelectLocation}\n      />\n\n      {/* Carousel Section - Overlay at bottom */}\n      <div className=\"absolute bottom-0 left-0 right-0 z-[1000]\">\n        <div\n          ref={carouselRef}\n          onMouseDown={handleMouseDown}\n          onMouseMove={handleMouseMove}\n          onMouseUp={handleMouseUp}\n          onMouseLeave={handleMouseLeave}\n          onTouchStart={handleTouchStart}\n          onTouchMove={handleTouchMove}\n          onTouchEnd={handleTouchEnd}\n          className={cn(\n            'flex gap-3 p-3 overflow-x-auto scrollbar-hide',\n            isDragging ? 'cursor-grabbing' : 'cursor-grab',\n            'select-none'\n          )}\n          style={{\n            scrollbarWidth: 'none',\n            msOverflowStyle: 'none',\n            WebkitOverflowScrolling: 'touch'\n          }}\n        >\n          {locations.map((location, index) => (\n            <div\n              key={index}\n              ref={(el) => {\n                if (el)\n                  cardRefs.current.set(\n                    index,\n                    el as unknown as HTMLButtonElement\n                  )\n              }}\n            >\n              <HotelCard\n                location={location}\n                isSelected={selectedIndex === index}\n                onClick={() => handleCardClick(location, index)}\n              />\n            </div>\n          ))}\n        </div>\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:block",
      "target": "components/ui/map-carousel.tsx"
    },
    {
      "path": "registry/map/demo/map.ts",
      "content": "// Demo data for Map category components\n// This file contains sample data used for component previews and documentation\n\nexport const demoMapLocations = [\n  {\n    id: '1',\n    name: 'The Embarcadero Grand',\n    subtitle: 'Embarcadero',\n    image: 'https://images.unsplash.com/photo-1566073771259-6a8506099945?w=400',\n    price: 329,\n    priceLabel: '$329 per night',\n    priceSubtext: 'USD · Includes taxes and fees',\n    rating: 9.1,\n    coordinates: [37.7935, -122.3938] as [number, number],\n  },\n  {\n    id: '2',\n    name: 'Hotel Nob Hill',\n    subtitle: 'Nob Hill',\n    image: 'https://images.unsplash.com/photo-1551882547-ff40c63fe5fa?w=400',\n    price: 275,\n    priceLabel: '$275 per night',\n    priceSubtext: 'USD · Includes taxes and fees',\n    rating: 8.7,\n    coordinates: [37.7925, -122.4138] as [number, number],\n  },\n  {\n    id: '3',\n    name: 'Marina Bay Suites',\n    subtitle: 'Marina District',\n    image: 'https://images.unsplash.com/photo-1582719478250-c89cae4dc85b?w=400',\n    price: 389,\n    priceLabel: '$389 per night',\n    priceSubtext: 'USD · Includes taxes and fees',\n    rating: 9.4,\n    coordinates: [37.8025, -122.4382] as [number, number],\n  },\n  {\n    id: '4',\n    name: 'Mission Street Inn',\n    subtitle: 'Mission District',\n    image: 'https://images.unsplash.com/photo-1520250497591-112f2f40a3f4?w=400',\n    price: 189,\n    priceLabel: '$189 per night',\n    priceSubtext: 'USD · Includes taxes and fees',\n    rating: 8.2,\n    coordinates: [37.7599, -122.4148] as [number, number],\n  },\n  {\n    id: '5',\n    name: 'The Hayes Valley Hotel',\n    subtitle: 'Hayes Valley',\n    image: 'https://images.unsplash.com/photo-1542314831-068cd1dbfeeb?w=400',\n    price: 245,\n    priceLabel: '$245 per night',\n    priceSubtext: 'USD · Includes taxes and fees',\n    rating: 8.9,\n    coordinates: [37.7759, -122.4245] as [number, number],\n  },\n]\n\nexport const demoMapCenter: [number, number] = [37.7749, -122.4194]\nexport const demoMapZoom = 12\n",
      "type": "registry:lib",
      "target": "components/ui/demo/map.ts"
    }
  ],
  "categories": [
    "map"
  ],
  "type": "registry:block"
}