{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "marker",
  "title": "Map Marker",
  "description": "Customizable map markers with popup support.",
  "dependencies": ["mapbox-gl", "lucide-react"],
  "devDependencies": ["@types/mapbox-gl"],
  "registryDependencies": ["https://www.terrae.dev/map.json"],
  "files": [
    {
      "path": "src/registry/map/marker.tsx",
      "content": "\"use client\"\n\nimport { createContext, useContext, useEffect, useRef, useState, type ReactNode } from \"react\"\nimport { createPortal } from \"react-dom\"\nimport type mapboxgl from \"mapbox-gl\"\nimport { mapgl } from \"./map-library\"\nimport { X } from \"lucide-react\"\nimport { cn } from \"@/lib/utils\"\nimport { useMap } from \"./hooks\"\nimport type { MapCoordinates, LngLatCoordinates } from \"./types\"\n\ntype MarkerContextValue = {\n  markerRef: React.RefObject<mapboxgl.Marker | null>\n  markerElementRef: React.RefObject<HTMLDivElement | null>\n  map: mapboxgl.Map | null\n  // State triggers re-render when marker element is ready.\n  // Refs don't cause re-renders, so children would see null without this.\n  isMounted: boolean\n}\n\ntype MapMarkerProps = {\n  coordinates: MapCoordinates\n  children: ReactNode\n  onClick?: (e: MouseEvent) => void\n  onMouseEnter?: (e: MouseEvent) => void\n  onMouseLeave?: (e: MouseEvent) => void\n  onDragStart?: (lngLat: LngLatCoordinates) => void\n  onDrag?: (lngLat: LngLatCoordinates) => void\n  onDragEnd?: (lngLat: LngLatCoordinates) => void\n  draggable?: boolean\n  offset?: [number, number]\n  rotation?: number\n  rotationAlignment?: \"map\" | \"viewport\" | \"auto\"\n  pitchAlignment?: \"map\" | \"viewport\" | \"auto\"\n}\n\ntype MarkerCallbacks = {\n  onClick?: (e: MouseEvent) => void\n  onMouseEnter?: (e: MouseEvent) => void\n  onMouseLeave?: (e: MouseEvent) => void\n  onDragStart?: (lngLat: LngLatCoordinates) => void\n  onDrag?: (lngLat: LngLatCoordinates) => void\n  onDragEnd?: (lngLat: LngLatCoordinates) => void\n}\n\ntype MarkerContentProps = {\n  children?: ReactNode\n  className?: string\n}\n\ntype MarkerPopupProps = {\n  children: ReactNode\n  className?: string\n  closeButton?: boolean\n  offset?: number | [number, number]\n  maxWidth?: string\n}\n\ntype MarkerTooltipProps = {\n  children: ReactNode\n  className?: string\n  offset?: number | [number, number]\n  maxWidth?: string\n}\n\ntype MarkerLabelProps = {\n  children: ReactNode\n  className?: string\n  position?: \"top\" | \"bottom\"\n}\n\ntype MarkerLabelPosition = \"top\" | \"bottom\"\n\ntype MarkerStatusColor = \"green\" | \"red\" | \"yellow\" | \"blue\"\n\ntype MarkerAvatarProps = {\n  src: string\n  alt: string\n  size?: number\n  online?: boolean\n  statusColor?: MarkerStatusColor\n  className?: string\n}\n\ntype MarkerAvatarPinProps = {\n  src: string\n  alt: string\n  size?: number\n  borderWidth?: number\n  className?: string\n}\n\nconst DEFAULT_DRAGGABLE = false\nconst DEFAULT_CLOSE_BUTTON = false\nconst DEFAULT_POPUP_OFFSET = 16\nconst DEFAULT_TOOLTIP_OFFSET = 16\nconst DEFAULT_LABEL_POSITION: MarkerLabelPosition = \"top\"\nconst DEFAULT_AVATAR_SIZE = 40\nconst DEFAULT_STATUS_COLOR: MarkerStatusColor = \"green\"\nconst DEFAULT_AVATAR_PIN_SIZE = 56\nconst DEFAULT_AVATAR_PIN_BORDER_WIDTH = 4\n\nconst LABEL_POSITION_CLASSES: Record<MarkerLabelPosition, string> = {\n  top: \"bottom-full mb-1\",\n  bottom: \"top-full mt-1\",\n}\n\nconst STATUS_COLORS: Record<MarkerStatusColor, string> = {\n  green: \"bg-green-500\",\n  red: \"bg-red-500\",\n  yellow: \"bg-yellow-500\",\n  blue: \"bg-blue-500\",\n}\n\nconst MarkerContext = createContext<MarkerContextValue | null>(null)\n\nconst useMarkerContext = () => {\n  const context = useContext(MarkerContext)\n  if (!context) {\n    throw new Error(\"Marker components must be used within MapMarker\")\n  }\n  return context\n}\n\nexport const MapMarker = ({\n  coordinates,\n  children,\n  onClick,\n  onMouseEnter,\n  onMouseLeave,\n  onDragStart,\n  onDrag,\n  onDragEnd,\n  draggable = DEFAULT_DRAGGABLE,\n  offset,\n  rotation,\n  rotationAlignment,\n  pitchAlignment,\n}: MapMarkerProps) => {\n  const { map, isLoaded } = useMap()\n  const markerRef = useRef<mapboxgl.Marker | null>(null)\n  const markerElementRef = useRef<HTMLDivElement | null>(null)\n  const [isMounted, setIsMounted] = useState(false)\n\n  const callbacksRef = useRef<MarkerCallbacks>({ onClick, onMouseEnter, onMouseLeave, onDragStart, onDrag, onDragEnd })\n\n  useEffect(() => {\n    callbacksRef.current = { onClick, onMouseEnter, onMouseLeave, onDragStart, onDrag, onDragEnd }\n  }, [onClick, onMouseEnter, onMouseLeave, onDragStart, onDrag, onDragEnd])\n\n  useEffect(() => {\n    if (!map || !isLoaded || !map.getContainer?.() || !map.getCanvasContainer?.()) {\n      return\n    }\n\n    const container = document.createElement(\"div\")\n    markerElementRef.current = container\n\n    const marker = new mapgl.Marker({\n      element: container,\n      draggable,\n      offset,\n      rotation,\n      rotationAlignment,\n      pitchAlignment,\n    })\n      .setLngLat(coordinates)\n      .addTo(map)\n\n    markerRef.current = marker\n\n    const handleClick = (e: MouseEvent) => {\n      callbacksRef.current.onClick?.(e)\n    }\n\n    const handleMouseEnter = (e: MouseEvent) => {\n      callbacksRef.current.onMouseEnter?.(e)\n    }\n\n    const handleMouseLeave = (e: MouseEvent) => {\n      callbacksRef.current.onMouseLeave?.(e)\n    }\n\n    const handleDragStart = () => {\n      const lngLat = markerRef.current?.getLngLat()\n      if (lngLat) {\n        callbacksRef.current.onDragStart?.({ lng: lngLat.lng, lat: lngLat.lat })\n      }\n    }\n\n    const handleDrag = () => {\n      const lngLat = markerRef.current?.getLngLat()\n      if (lngLat) {\n        callbacksRef.current.onDrag?.({ lng: lngLat.lng, lat: lngLat.lat })\n      }\n    }\n\n    const handleDragEnd = () => {\n      const lngLat = markerRef.current?.getLngLat()\n      if (lngLat) {\n        callbacksRef.current.onDragEnd?.({ lng: lngLat.lng, lat: lngLat.lat })\n      }\n    }\n\n    container.addEventListener(\"click\", handleClick)\n    container.addEventListener(\"mouseenter\", handleMouseEnter)\n    container.addEventListener(\"mouseleave\", handleMouseLeave)\n\n    marker.on(\"dragstart\", handleDragStart)\n    marker.on(\"drag\", handleDrag)\n    marker.on(\"dragend\", handleDragEnd)\n\n    setIsMounted(true)\n\n    return () => {\n      container.removeEventListener(\"click\", handleClick)\n      container.removeEventListener(\"mouseenter\", handleMouseEnter)\n      container.removeEventListener(\"mouseleave\", handleMouseLeave)\n\n      marker.off(\"dragstart\", handleDragStart)\n      marker.off(\"drag\", handleDrag)\n      marker.off(\"dragend\", handleDragEnd)\n\n      marker.remove()\n      markerRef.current = null\n      markerElementRef.current = null\n      setIsMounted(false)\n    }\n  }, [map, isLoaded])\n\n  useEffect(() => {\n    if (!markerRef.current) {\n      return\n    }\n\n    markerRef.current.setLngLat(coordinates)\n  }, [coordinates])\n\n  useEffect(() => {\n    if (!markerRef.current) {\n      return\n    }\n\n    markerRef.current.setDraggable(draggable)\n  }, [draggable])\n\n  useEffect(() => {\n    if (!markerRef.current) {\n      return\n    }\n\n    if (offset) {\n      markerRef.current.setOffset(offset)\n    }\n  }, [offset])\n\n  useEffect(() => {\n    if (!markerRef.current) {\n      return\n    }\n\n    if (rotation !== undefined) {\n      markerRef.current.setRotation(rotation)\n    }\n  }, [rotation])\n\n  useEffect(() => {\n    if (!markerRef.current) {\n      return\n    }\n\n    if (rotationAlignment) {\n      markerRef.current.setRotationAlignment(rotationAlignment)\n    }\n  }, [rotationAlignment])\n\n  useEffect(() => {\n    if (!markerRef.current) {\n      return\n    }\n\n    if (pitchAlignment) {\n      markerRef.current.setPitchAlignment(pitchAlignment)\n    }\n  }, [pitchAlignment])\n\n  return (\n    <MarkerContext.Provider value={{ markerRef, markerElementRef, map, isMounted }}>{children}</MarkerContext.Provider>\n  )\n}\n\nconst DefaultMarkerIcon = () => {\n  return <div className=\"relative h-4 w-4 rounded-full border-2 border-white bg-blue-500 shadow-lg\" />\n}\n\nexport const MarkerContent = ({ children, className }: MarkerContentProps) => {\n  const { markerElementRef, isMounted } = useMarkerContext()\n\n  if (!isMounted || !markerElementRef.current) {\n    return null\n  }\n\n  return createPortal(\n    <div className={cn(\"relative cursor-pointer\", className)}>{children || <DefaultMarkerIcon />}</div>,\n    markerElementRef.current\n  )\n}\n\nexport const MarkerPopup = ({\n  children,\n  className,\n  closeButton = DEFAULT_CLOSE_BUTTON,\n  offset = DEFAULT_POPUP_OFFSET,\n  maxWidth,\n}: MarkerPopupProps) => {\n  const { markerRef, isMounted } = useMarkerContext()\n  const containerRef = useRef<HTMLDivElement | null>(null)\n  const popupRef = useRef<mapboxgl.Popup | null>(null)\n  const [mounted, setMounted] = useState(false)\n\n  useEffect(() => {\n    if (!isMounted || !markerRef.current) {\n      return\n    }\n\n    const container = document.createElement(\"div\")\n    containerRef.current = container\n\n    const popup = new mapgl.Popup({\n      offset,\n      closeButton: false,\n      className: \"custom-map-popup\",\n    })\n      .setMaxWidth(maxWidth || \"none\")\n      .setDOMContent(container)\n\n    popupRef.current = popup\n    markerRef.current.setPopup(popup)\n    setMounted(true)\n\n    return () => {\n      popup.remove()\n      popupRef.current = null\n      containerRef.current = null\n      setMounted(false)\n    }\n  }, [isMounted, offset, maxWidth])\n\n  const handleClose = () => {\n    popupRef.current?.remove()\n  }\n\n  if (!mounted || !containerRef.current) {\n    return null\n  }\n\n  return createPortal(\n    <div\n      className={cn(\n        \"relative rounded-md border bg-popover p-3 text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95\",\n        className\n      )}\n    >\n      {closeButton && (\n        <button\n          type=\"button\"\n          onClick={handleClose}\n          className=\"absolute top-1 right-1 z-10 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2\"\n          aria-label=\"Close popup\"\n        >\n          <X className=\"h-4 w-4\" />\n          <span className=\"sr-only\">Close</span>\n        </button>\n      )}\n      {children}\n    </div>,\n    containerRef.current\n  )\n}\n\nexport const MarkerTooltip = ({\n  children,\n  className,\n  offset = DEFAULT_TOOLTIP_OFFSET,\n  maxWidth,\n}: MarkerTooltipProps) => {\n  const { markerRef, markerElementRef, map, isMounted } = useMarkerContext()\n  const containerRef = useRef<HTMLDivElement | null>(null)\n  const popupRef = useRef<mapboxgl.Popup | null>(null)\n  const [mounted, setMounted] = useState(false)\n\n  useEffect(() => {\n    if (!isMounted || !markerRef.current || !markerElementRef.current || !map) {\n      return\n    }\n\n    const container = document.createElement(\"div\")\n    containerRef.current = container\n\n    const popup = new mapgl.Popup({\n      offset,\n      closeOnClick: true,\n      closeButton: false,\n      className: \"custom-map-popup\",\n    })\n      .setMaxWidth(maxWidth || \"none\")\n      .setDOMContent(container)\n\n    popupRef.current = popup\n\n    const markerElement = markerElementRef.current\n    const marker = markerRef.current\n\n    const handleMouseEnter = () => {\n      popup.setLngLat(marker.getLngLat()).addTo(map)\n    }\n\n    const handleMouseLeave = () => {\n      popup.remove()\n    }\n\n    markerElement.addEventListener(\"mouseenter\", handleMouseEnter)\n    markerElement.addEventListener(\"mouseleave\", handleMouseLeave)\n    setMounted(true)\n\n    return () => {\n      markerElement.removeEventListener(\"mouseenter\", handleMouseEnter)\n      markerElement.removeEventListener(\"mouseleave\", handleMouseLeave)\n      popup.remove()\n      popupRef.current = null\n      containerRef.current = null\n      setMounted(false)\n    }\n  }, [isMounted, map, offset, maxWidth])\n\n  if (!mounted || !containerRef.current) {\n    return null\n  }\n\n  return createPortal(\n    <div\n      className={cn(\n        \"rounded-md bg-foreground px-2 py-1 text-xs text-background shadow-md animate-in fade-in-0 zoom-in-95\",\n        className\n      )}\n    >\n      {children}\n    </div>,\n    containerRef.current\n  )\n}\n\nexport const MarkerLabel = ({ children, className, position = DEFAULT_LABEL_POSITION }: MarkerLabelProps) => {\n  return (\n    <div\n      className={cn(\n        \"absolute left-1/2 -translate-x-1/2 whitespace-nowrap\",\n        \"text-[10px] font-medium text-foreground\",\n        LABEL_POSITION_CLASSES[position],\n        className\n      )}\n    >\n      {children}\n    </div>\n  )\n}\n\nexport const MarkerAvatar = ({\n  src,\n  alt,\n  size = DEFAULT_AVATAR_SIZE,\n  online,\n  statusColor = DEFAULT_STATUS_COLOR,\n  className,\n}: MarkerAvatarProps) => {\n  return (\n    <div className=\"relative flex items-center justify-center\">\n      <div\n        className=\"absolute rounded-full bg-primary/10 animate-pulse\"\n        style={{\n          width: size * 1.4,\n          height: size * 1.4,\n        }}\n      />\n\n      <div\n        className={cn(\"relative rounded-full border-2 border-background shadow-lg overflow-hidden\", className)}\n        style={{ width: size, height: size }}\n      >\n        <img src={src} alt={alt} className=\"w-full h-full object-cover\" />\n      </div>\n\n      {online !== undefined && (\n        <div\n          className={cn(\n            \"absolute bottom-0 right-0 rounded-full border-2 border-background\",\n            STATUS_COLORS[statusColor],\n            online && \"animate-pulse\"\n          )}\n          style={{\n            width: size * 0.25,\n            height: size * 0.25,\n          }}\n        />\n      )}\n    </div>\n  )\n}\n\nexport const MarkerAvatarPin = ({\n  src,\n  alt,\n  size = DEFAULT_AVATAR_PIN_SIZE,\n  borderWidth = DEFAULT_AVATAR_PIN_BORDER_WIDTH,\n  className,\n}: MarkerAvatarPinProps) => {\n  const pinSize = size * 0.32\n  const pinOffset = pinSize * 0.35\n\n  return (\n    <div className={cn(\"relative flex flex-col items-center\", className)}>\n      <div\n        className=\"rounded-full border-background bg-background shadow-lg overflow-hidden\"\n        style={{\n          width: size,\n          height: size,\n          borderWidth,\n          borderStyle: \"solid\",\n        }}\n      >\n        <img src={src} alt={alt} className=\"w-full h-full object-cover\" />\n      </div>\n      <div\n        className=\"absolute bg-background rotate-45 rounded-sm\"\n        style={{\n          width: pinSize,\n          height: pinSize,\n          bottom: -pinOffset,\n        }}\n      />\n    </div>\n  )\n}\n",
      "type": "registry:ui",
      "target": "components/ui/map/marker.tsx"
    }
  ],
  "type": "registry:ui"
}
