{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "store-locator",
  "title": "Store Locator",
  "description": "Searchable store list synced with map markers.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "@mapcn/map",
    "input",
    "sidebar"
  ],
  "files": [
    {
      "path": "src/registry/blocks/store-locator/page.tsx",
      "content": "\"use client\";\n\nimport { useMemo, useState, type CSSProperties } from \"react\";\n\nimport { SidebarInset, SidebarProvider } from \"@/components/ui/sidebar\";\nimport { StoreList } from \"./components/store-list\";\nimport { LocatorMap } from \"./components/locator-map\";\nimport { MAP_CENTER, stores } from \"./data\";\n\nexport default function Page() {\n  const [query, setQuery] = useState(\"\");\n  const [selectedId, setSelectedId] = useState<string | null>(stores[0].id);\n\n  const filtered = useMemo(() => {\n    const q = query.trim().toLowerCase();\n    if (!q) return stores;\n    return stores.filter(\n      (store) =>\n        store.name.toLowerCase().includes(q) ||\n        store.address.toLowerCase().includes(q) ||\n        store.neighborhood.toLowerCase().includes(q),\n    );\n  }, [query]);\n\n  return (\n    <SidebarProvider style={{ \"--sidebar-width\": \"20rem\" } as CSSProperties}>\n      <StoreList\n        stores={filtered}\n        query={query}\n        onQueryChange={setQuery}\n        selectedId={selectedId}\n        onSelect={setSelectedId}\n      />\n\n      <SidebarInset>\n        <LocatorMap\n          stores={filtered}\n          selectedId={selectedId}\n          onSelect={setSelectedId}\n          onClearSelection={() => setSelectedId(null)}\n          center={MAP_CENTER}\n        />\n      </SidebarInset>\n    </SidebarProvider>\n  );\n}\n",
      "type": "registry:page",
      "target": "app/store-locator/page.tsx"
    },
    {
      "path": "src/registry/blocks/store-locator/data.ts",
      "content": "export interface Store {\n  id: string;\n  name: string;\n  address: string;\n  neighborhood: string;\n  phone: string;\n  hours: string;\n  openNow: boolean;\n  lat: number;\n  lng: number;\n}\n\nexport const MAP_CENTER: [number, number] = [-122.4194, 37.7749];\n\nexport const stores: Store[] = [\n  {\n    id: \"ferry-building\",\n    name: \"Ferry Building\",\n    address: \"1 Ferry Building\",\n    neighborhood: \"Embarcadero\",\n    phone: \"(415) 555-0142\",\n    hours: \"7:00 AM – 8:00 PM\",\n    openNow: true,\n    lat: 37.7955,\n    lng: -122.3933,\n  },\n  {\n    id: \"union-square\",\n    name: \"Union Square\",\n    address: \"333 Post St\",\n    neighborhood: \"Downtown\",\n    phone: \"(415) 555-0188\",\n    hours: \"6:30 AM – 9:00 PM\",\n    openNow: true,\n    lat: 37.788,\n    lng: -122.4075,\n  },\n  {\n    id: \"north-beach\",\n    name: \"North Beach\",\n    address: \"601 Columbus Ave\",\n    neighborhood: \"North Beach\",\n    phone: \"(415) 555-0119\",\n    hours: \"7:00 AM – 7:00 PM\",\n    openNow: true,\n    lat: 37.8003,\n    lng: -122.4097,\n  },\n  {\n    id: \"hayes-valley\",\n    name: \"Hayes Valley\",\n    address: \"450 Hayes St\",\n    neighborhood: \"Hayes Valley\",\n    phone: \"(415) 555-0163\",\n    hours: \"7:30 AM – 6:00 PM\",\n    openNow: false,\n    lat: 37.7765,\n    lng: -122.4256,\n  },\n  {\n    id: \"mission\",\n    name: \"Mission District\",\n    address: \"2901 Mission St\",\n    neighborhood: \"Mission\",\n    phone: \"(415) 555-0177\",\n    hours: \"6:00 AM – 8:00 PM\",\n    openNow: true,\n    lat: 37.7509,\n    lng: -122.4181,\n  },\n  {\n    id: \"marina\",\n    name: \"Marina\",\n    address: \"2156 Chestnut St\",\n    neighborhood: \"Marina\",\n    phone: \"(415) 555-0125\",\n    hours: \"7:00 AM – 7:00 PM\",\n    openNow: false,\n    lat: 37.8003,\n    lng: -122.4366,\n  },\n  {\n    id: \"castro\",\n    name: \"Castro\",\n    address: \"489 Castro St\",\n    neighborhood: \"Castro\",\n    phone: \"(415) 555-0151\",\n    hours: \"7:00 AM – 8:00 PM\",\n    openNow: true,\n    lat: 37.7609,\n    lng: -122.435,\n  },\n  {\n    id: \"soma\",\n    name: \"SoMa\",\n    address: \"680 Folsom St\",\n    neighborhood: \"SoMa\",\n    phone: \"(415) 555-0134\",\n    hours: \"6:30 AM – 6:30 PM\",\n    openNow: true,\n    lat: 37.7852,\n    lng: -122.3982,\n  },\n];\n",
      "type": "registry:component",
      "target": "app/store-locator/data.ts"
    },
    {
      "path": "src/registry/blocks/store-locator/components/store-list.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef } from \"react\";\nimport { Clock, MapPin, Phone, Search } from \"lucide-react\";\n\nimport {\n  Sidebar,\n  SidebarContent,\n  SidebarGroup,\n  SidebarHeader,\n  SidebarMenu,\n  SidebarMenuButton,\n  SidebarMenuItem,\n  SidebarSeparator,\n} from \"@/components/ui/sidebar\";\nimport { Input } from \"@/components/ui/input\";\nimport { cn } from \"@/lib/utils\";\nimport type { Store } from \"../data\";\n\ninterface StoreListProps {\n  stores: Store[];\n  query: string;\n  onQueryChange: (value: string) => void;\n  selectedId: string | null;\n  onSelect: (id: string) => void;\n}\n\nexport function StoreList({\n  stores,\n  query,\n  onQueryChange,\n  selectedId,\n  onSelect,\n}: StoreListProps) {\n  const itemRefs = useRef(new Map<string, HTMLButtonElement>());\n\n  useEffect(() => {\n    if (!selectedId) return;\n    itemRefs.current\n      .get(selectedId)\n      ?.scrollIntoView({ block: \"nearest\", behavior: \"smooth\" });\n  }, [selectedId]);\n\n  return (\n    <Sidebar collapsible=\"offcanvas\">\n      <SidebarHeader className=\"gap-3 p-4\">\n        <div>\n          <h2 className=\"text-foreground text-lg font-semibold tracking-tight\">\n            Find a store\n          </h2>\n          <p className=\"text-muted-foreground text-sm\">\n            {stores.length} {stores.length === 1 ? \"location\" : \"locations\"}{\" \"}\n            near you\n          </p>\n        </div>\n        <div className=\"relative\">\n          <Search className=\"text-muted-foreground absolute top-1/2 left-2.5 size-4 -translate-y-1/2\" />\n          <Input\n            value={query}\n            onChange={(e) => onQueryChange(e.target.value)}\n            placeholder=\"Search name or address\"\n            className=\"bg-background pl-8\"\n            aria-label=\"Search stores\"\n          />\n        </div>\n      </SidebarHeader>\n\n      <SidebarSeparator className=\"mx-0\" />\n\n      <SidebarContent>\n        {stores.length === 0 ? (\n          <div className=\"text-muted-foreground p-6 text-center text-sm\">\n            No stores match your search.\n          </div>\n        ) : (\n          <SidebarGroup className=\"p-2\">\n            <SidebarMenu className=\"gap-1\">\n              {stores.map((store) => {\n                const active = store.id === selectedId;\n                return (\n                  <SidebarMenuItem key={store.id}>\n                    <SidebarMenuButton\n                      asChild\n                      isActive={active}\n                      className=\"h-auto flex-col items-stretch gap-0 p-3\"\n                    >\n                      <button\n                        type=\"button\"\n                        ref={(el) => {\n                          if (el) itemRefs.current.set(store.id, el);\n                          else itemRefs.current.delete(store.id);\n                        }}\n                        onClick={() => onSelect(store.id)}\n                        aria-current={active}\n                      >\n                        <div className=\"flex items-center justify-between gap-2\">\n                          <span className=\"text-foreground font-medium\">\n                            {store.name}\n                          </span>\n                          <span\n                            className={cn(\n                              \"flex items-center gap-1.5 text-xs font-medium\",\n                              store.openNow\n                                ? \"text-foreground\"\n                                : \"text-muted-foreground\",\n                            )}\n                          >\n                            <span\n                              className={cn(\n                                \"size-1.5 rounded-full\",\n                                store.openNow\n                                  ? \"bg-emerald-500\"\n                                  : \"bg-neutral-500\",\n                              )}\n                            />\n                            {store.openNow ? \"Open\" : \"Closed\"}\n                          </span>\n                        </div>\n\n                        <div className=\"text-muted-foreground mt-2 space-y-1.5 text-xs font-normal tabular-nums\">\n                          <p className=\"flex items-center gap-1.5\">\n                            <MapPin className=\"size-3.5 shrink-0\" />\n                            {store.address}, {store.neighborhood}\n                          </p>\n                          <p className=\"flex items-center gap-1.5\">\n                            <Clock className=\"size-3.5 shrink-0\" />\n                            {store.hours}\n                          </p>\n                          <p className=\"flex items-center gap-1.5\">\n                            <Phone className=\"size-3.5 shrink-0\" />\n                            {store.phone}\n                          </p>\n                        </div>\n                      </button>\n                    </SidebarMenuButton>\n                  </SidebarMenuItem>\n                );\n              })}\n            </SidebarMenu>\n          </SidebarGroup>\n        )}\n      </SidebarContent>\n    </Sidebar>\n  );\n}\n",
      "type": "registry:component",
      "target": "app/store-locator/components/store-list.tsx"
    },
    {
      "path": "src/registry/blocks/store-locator/components/locator-map.tsx",
      "content": "\"use client\";\n\nimport { useEffect } from \"react\";\nimport { Clock, MapPin, Phone } from \"lucide-react\";\n\nimport {\n  Map,\n  MapControls,\n  MapMarker,\n  MapPopup,\n  MarkerContent,\n  MarkerTooltip,\n  useMap,\n} from \"@/components/ui/map\";\nimport { SidebarTrigger } from \"@/components/ui/sidebar\";\nimport { cn } from \"@/lib/utils\";\nimport type { Store } from \"../data\";\n\ninterface LocatorMapProps {\n  stores: Store[];\n  selectedId: string | null;\n  onSelect: (id: string) => void;\n  onClearSelection: () => void;\n  center: [number, number];\n}\n\nfunction FlyToSelected({ store }: { store?: Store }) {\n  const { map } = useMap();\n\n  useEffect(() => {\n    if (!map || !store) return;\n    map.flyTo({\n      center: [store.lng, store.lat],\n      zoom: 14,\n      duration: 800,\n      essential: true,\n    });\n  }, [map, store]);\n\n  return null;\n}\n\nfunction StorePin({ active }: { active: boolean }) {\n  return (\n    <div\n      className={cn(\n        \"text-background flex items-center justify-center rounded-full shadow-md transition-all\",\n        active\n          ? \"bg-foreground/80 size-9 scale-110\"\n          : \"bg-foreground hover:bg-muted-foreground size-7\",\n      )}\n    >\n      <MapPin className={cn(active ? \"size-4.5\" : \"size-3.5\")} />\n    </div>\n  );\n}\n\nexport function LocatorMap({\n  stores,\n  selectedId,\n  onSelect,\n  onClearSelection,\n  center,\n}: LocatorMapProps) {\n  const selected = stores.find((store) => store.id === selectedId);\n\n  return (\n    <div className=\"relative h-full\">\n      <SidebarTrigger className=\"bg-background absolute top-3 left-3 z-10 border shadow-sm md:hidden\" />\n\n      <Map center={center} zoom={12} minZoom={10} maxZoom={17}>\n        <MapControls showFullscreen showCompass />\n        <FlyToSelected store={selected} />\n\n        {stores.map((store) => (\n          <MapMarker\n            key={store.id}\n            longitude={store.lng}\n            latitude={store.lat}\n            onClick={() => onSelect(store.id)}\n          >\n            <MarkerContent>\n              <StorePin active={store.id === selectedId} />\n            </MarkerContent>\n            <MarkerTooltip\n              offset={24}\n              className=\"bg-foreground text-background\"\n            >\n              {store.name}\n            </MarkerTooltip>\n          </MapMarker>\n        ))}\n\n        {selected && (\n          <MapPopup\n            longitude={selected.lng}\n            latitude={selected.lat}\n            offset={26}\n            closeButton\n            closeOnClick={false}\n            onClose={onClearSelection}\n            className=\"min-w-56\"\n            focusAfterOpen={false}\n          >\n            <p className=\"text-popover-foreground pr-5 font-medium\">\n              {selected.name}\n            </p>\n            <span\n              className={cn(\n                \"mt-1 flex items-center gap-1.5 text-xs font-medium\",\n                selected.openNow ? \"text-foreground\" : \"text-muted-foreground\",\n              )}\n            >\n              <span\n                className={cn(\n                  \"size-1.5 rounded-full\",\n                  selected.openNow ? \"bg-emerald-500\" : \"bg-neutral-500\",\n                )}\n              />\n              {selected.openNow ? \"Open now\" : \"Closed\"}\n            </span>\n\n            <div className=\"text-muted-foreground mt-2.5 space-y-1.5 text-xs tabular-nums\">\n              <p className=\"flex items-center gap-1.5\">\n                <MapPin className=\"size-3.5 shrink-0\" />\n                {selected.address}, {selected.neighborhood}\n              </p>\n              <p className=\"flex items-center gap-1.5\">\n                <Clock className=\"size-3.5 shrink-0\" />\n                {selected.hours}\n              </p>\n              <p className=\"flex items-center gap-1.5\">\n                <Phone className=\"size-3.5 shrink-0\" />\n                {selected.phone}\n              </p>\n            </div>\n          </MapPopup>\n        )}\n      </Map>\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "app/store-locator/components/locator-map.tsx"
    }
  ],
  "meta": {
    "iframeHeight": "800px"
  },
  "categories": [
    "store-locator",
    "locator"
  ],
  "type": "registry:block"
}
