shadcn.io is not affiliated with official shadcn/ui
Store Locator Clusters Map Block for React Aria
A store locator on a pan-and-zoom drawn valley: fourteen stores merge into count circles when zoomed out and split as you zoom in, with a search, a Show open at select that dashes closed stores, and a nearest-first list.
Installation
Overview
Help shoppers find the nearest open shop, with this store locator clusters map for React with React Aria. Linnet & Loom, a homeware shop, draws fourteen stores across an invented valley, Corrin Valley, on an SVG map that pans and zooms inside its own box. Zoomed out, nearby stores merge into circles with a count; each press of Zoom in splits them further until every store shows as a square with its two-letter mark. A mouse or pen drags the map, and with the map focused the arrow keys pan it, plus and minus zoom it and 0 resets it; three buttons do the same for touch, because touch keeps scrolling the page and the wheel never zooms. A search Input filters by name or area, a Show open at Select (Monday 10:00, Monday 20:00, Sunday 11:00) dashes the stores that are closed then, and the list at the side gives each store's distance from a typed home pin and its hours; pressing a row centres the store. Clusters are a greedy merge by distance in the drawing, not geography; the stores and hours are sample data. For shops, clinics, branches and any finder with many points.
FAQ
How do the clusters work?
groupsOf walks the stores that pass the search and puts each in the first group whose centre is within a merge radius of it (70 drawing units at zoom 1, 30 at zoom 2, none at zoom 3), moving the group's centre to the mean. A group of one is a store; a larger group is a circle with its count, and pressing it zooms in on its centre. It is a plain greedy merge, which is enough for fourteen points; use a grid or a k-d tree for thousands.
How do pan and zoom work without moving the page?
The view is { k, tx, ty }: the zoom (1 to 3) and the shift as a percentage of the map's own box. The ground is one element with a CSS transform from those values; the pins are positioned from the same numbers, so they keep their size. The shift is clamped so the ground always covers the map. Only a mouse or pen drags; touch keeps scrolling the page, and the wheel never zooms.
Can I use it without a mouse?
Yes. The map is one focusable region with a hint tied to it: arrow keys pan, plus and minus zoom, 0 resets. Zoom in, Zoom out and Reset are named buttons. Every store is also a row in the list, a toggle button with its hours and distance, which is the keyboard route; the pins are hidden from assistive technology. A polite message speaks the zoom level, the number of stores and pins, and how many are open.
How is open at a time decided?
Each store has weekday and Sunday hours in minutes after midnight (a Sunday of zero means closed). The select picks one moment, and a store is open when the moment falls from its opening time up to its closing time. A closed store gets a dashed outline on the map and "Closed, opens 09:00" or "Closed since 18:00" in its row; nothing is removed.
What stays local in the demo, and how do I wire it up?
All of it. The stores, hours and the home pin are typed sample data, and nothing asks for the visitor's location. In your app feed STORES from your store data, set the home pin from a postcode lookup you run on your server, and replace the drawing with your map library while keeping groupsOf or its own clustering.
How does it look on a phone?
The search and the select stack above the map, the map comes first at a fixed height with the three zoom buttons at its end, and the list sits under it in a box that scrolls on its own. Dragging the map with a finger scrolls the page, as intended; use the buttons to zoom and the list to centre a store.