{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "map-pin",
  "title": "Map pin",
  "author": "Tori Bryan",
  "description": "A pin for a point on a map, with a preview that opens on click or tap.",
  "dependencies": [
    "@base-ui/react",
    "class-variance-authority",
    "lucide-react"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/ui/map-pin.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Popover as PopoverPrimitive } from \"@base-ui/react/popover\"\nimport { cva } from \"class-variance-authority\"\nimport { MapPinIcon } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\ntype MapPinProps = Omit<\n  React.ComponentProps<\"button\">,\n  \"type\" | \"title\" | \"children\"\n> & {\n  /** The place's name. It's the pin's accessible name, and the text of a label pin unless `text` is set. */\n  label: string\n  /** `dot` is a small point, `icon` a round pin with an icon, `label` a pill with text such as a price. */\n  type?: \"dot\" | \"icon\" | \"label\"\n  /** What the color says: `default` for plain places, or a status such as `success` for open or `destructive` for closed. */\n  variant?: \"default\" | \"success\" | \"warning\" | \"info\" | \"destructive\"\n  /** The icon for an `icon` pin, or a leading icon in a `label` pin. An `icon` pin falls back to a map-pin icon. */\n  icon?: React.ReactNode\n  /** What a label pin shows, when it differs from `label`, such as \"$120\". */\n  text?: React.ReactNode\n  /** `sm` for dense maps. */\n  size?: \"sm\" | \"default\"\n  /** A photo at the top of the preview. */\n  image?: { src: string; alt: string }\n  /** The preview's heading. */\n  title?: React.ReactNode\n  /** A line or two under the title. */\n  description?: React.ReactNode\n  /** A small label above the title, such as a category or distance. */\n  meta?: React.ReactNode\n  /** Anything else for the preview, under the other fields: links, actions. */\n  children?: React.ReactNode\n  /** Whether the preview is open, when controlled. */\n  open?: boolean\n  /** Whether the preview starts open, when uncontrolled. */\n  defaultOpen?: boolean\n  /** Called when the preview opens or closes. */\n  onOpenChange?: (open: boolean) => void\n  /** Which side of the pin the preview opens on. It flips when there's no room. */\n  side?: \"top\" | \"right\" | \"bottom\" | \"left\"\n  /** Classes for the preview. It's portalled to the body, out of reach of the pin's selectors. */\n  previewClassName?: string\n}\n\ntype Variant = NonNullable<MapPinProps[\"variant\"]>\n\n// Filled pins, dots and icons, take the color as their fill and grow a\n// halo of it when open.\nconst FILL: Record<Variant, string> = {\n  default:\n    \"bg-primary text-primary-foreground data-[popup-open]:shadow-[0_0_0_6px_var(--color-primary-subtle)]\",\n  success:\n    \"bg-success text-success-foreground data-[popup-open]:shadow-[0_0_0_6px_var(--color-success-subtle)]\",\n  warning:\n    \"bg-warning text-warning-foreground data-[popup-open]:shadow-[0_0_0_6px_var(--color-warning-subtle)]\",\n  info: \"bg-info text-info-foreground data-[popup-open]:shadow-[0_0_0_6px_var(--color-info-subtle)]\",\n  destructive:\n    \"bg-destructive text-destructive-foreground data-[popup-open]:shadow-[0_0_0_6px_var(--color-destructive-subtle)]\",\n}\n\n// A label stays light on the map with its text in the color, and fills in\n// when open, so the selected one stands out among the rest.\nconst OUTLINE: Record<Variant, string> = {\n  default:\n    \"text-foreground data-[popup-open]:border-primary data-[popup-open]:bg-primary data-[popup-open]:text-primary-foreground\",\n  success:\n    \"text-success data-[popup-open]:border-success data-[popup-open]:bg-success data-[popup-open]:text-success-foreground\",\n  warning:\n    \"text-warning data-[popup-open]:border-warning data-[popup-open]:bg-warning data-[popup-open]:text-warning-foreground\",\n  info: \"text-info data-[popup-open]:border-info data-[popup-open]:bg-info data-[popup-open]:text-info-foreground\",\n  destructive:\n    \"text-destructive data-[popup-open]:border-destructive data-[popup-open]:bg-destructive data-[popup-open]:text-destructive-foreground\",\n}\n\nconst VARIANTS = Object.keys(FILL) as Variant[]\n\nconst mapPinVariants = cva(\n  // The spring curve overshoots a little, so a pin settles into its open\n  // size rather than stopping dead. Hover lifts a pin part of the way to\n  // its open size, and stays off the open one so its selected look holds.\n  \"relative inline-flex shrink-0 cursor-pointer touch-manipulation items-center justify-center transition-[scale,translate,background-color,border-color,color,box-shadow] duration-300 ease-[cubic-bezier(0.34,1.56,0.64,1)] outline-none select-none hover:not-data-[popup-open]:shadow-md focus-visible:ring-[3px] focus-visible:ring-ring-subtle motion-reduce:transition-none\",\n  {\n    variants: {\n      type: {\n        // The dot is small to look at, so its press area reaches past it.\n        dot: \"rounded-full shadow-sm ring-2 ring-background before:absolute before:-inset-2 before:rounded-full hover:not-data-[popup-open]:scale-110 data-[popup-open]:scale-125\",\n        icon: \"rounded-full shadow-sm ring-2 ring-background hover:not-data-[popup-open]:scale-105 data-[popup-open]:scale-110 [&_svg]:shrink-0\",\n        label:\n          \"gap-1 rounded-full border border-border bg-background font-semibold whitespace-nowrap shadow-sm hover:not-data-[popup-open]:scale-[1.03] hover:not-data-[popup-open]:bg-muted data-[popup-open]:scale-105 [&_svg]:shrink-0\",\n      },\n      variant: {\n        default: \"\",\n        success: \"\",\n        warning: \"\",\n        info: \"\",\n        destructive: \"\",\n      },\n      size: { sm: \"\", default: \"\" },\n    },\n    compoundVariants: [\n      { type: \"dot\", size: \"sm\", className: \"size-3\" },\n      { type: \"dot\", size: \"default\", className: \"size-4\" },\n      { type: \"icon\", size: \"sm\", className: \"size-6 [&_svg]:size-3\" },\n      { type: \"icon\", size: \"default\", className: \"size-7 [&_svg]:size-3.5\" },\n      {\n        type: \"label\",\n        size: \"sm\",\n        className: \"h-6 px-2 text-[11px] [&_svg]:size-3\",\n      },\n      {\n        type: \"label\",\n        size: \"default\",\n        className: \"h-7 px-2.5 text-xs [&_svg]:size-3.5\",\n      },\n      ...VARIANTS.flatMap((variant) => [\n        { type: \"dot\" as const, variant, className: FILL[variant] },\n        { type: \"icon\" as const, variant, className: FILL[variant] },\n        { type: \"label\" as const, variant, className: OUTLINE[variant] },\n      ]),\n    ],\n    defaultVariants: { type: \"dot\", variant: \"default\", size: \"default\" },\n  }\n)\n\n/**\n * A pin for a point on a map, with a preview that opens on click or tap.\n * It brings no map of its own: render it inside your map library's marker,\n * which places it and moves it as the map pans and zooms.\n */\nfunction MapPin({\n  label,\n  type = \"dot\",\n  variant = \"default\",\n  icon,\n  text,\n  size = \"default\",\n  image,\n  title,\n  description,\n  meta,\n  children,\n  open,\n  defaultOpen,\n  onOpenChange,\n  side = \"top\",\n  previewClassName,\n  className,\n  ...props\n}: MapPinProps) {\n  const shown =\n    type === \"icon\" ? (\n      (icon ?? <MapPinIcon />)\n    ) : type === \"label\" ? (\n      <>\n        {icon}\n        {text ?? label}\n      </>\n    ) : null\n\n  const pin = (\n    <button\n      type=\"button\"\n      data-slot=\"map-pin\"\n      data-type={type}\n      data-variant={variant}\n      data-size={size}\n      aria-label={type === \"label\" && text === undefined ? undefined : label}\n      className={cn(mapPinVariants({ type, variant, size }), className)}\n      {...props}\n    >\n      {shown}\n    </button>\n  )\n\n  const hasPreview =\n    image !== undefined ||\n    title !== undefined ||\n    description !== undefined ||\n    meta !== undefined ||\n    children !== undefined\n  if (!hasPreview) return pin\n\n  return (\n    <PopoverPrimitive.Root\n      open={open}\n      defaultOpen={defaultOpen}\n      onOpenChange={(next) => onOpenChange?.(next)}\n    >\n      <PopoverPrimitive.Trigger render={pin} />\n      <PopoverPrimitive.Portal>\n        <PopoverPrimitive.Positioner\n          side={side}\n          sideOffset={type === \"dot\" ? 12 : 8}\n          collisionPadding={8}\n          className=\"isolate z-50\"\n        >\n          <PopoverPrimitive.Popup\n            data-slot=\"map-pin-preview\"\n            aria-label={label}\n            className={cn(\n              \"group/preview w-64 max-w-[calc(100vw-1rem)] origin-(--transform-origin) overflow-hidden rounded-xl border border-border bg-popover text-popover-foreground shadow-lg outline-hidden\",\n              // Grows out of the pin with a little overshoot, and leaves\n              // faster than it came, so closing never holds anything up.\n              \"transition-[opacity,scale,translate] duration-300 ease-[cubic-bezier(0.34,1.56,0.64,1)] data-ending-style:duration-150 data-ending-style:ease-in\",\n              \"data-ending-style:scale-95 data-ending-style:opacity-0 data-starting-style:scale-[0.85] data-starting-style:opacity-0\",\n              \"data-[side=bottom]:data-starting-style:-translate-y-2 data-[side=left]:data-starting-style:translate-x-2 data-[side=right]:data-starting-style:-translate-x-2 data-[side=top]:data-starting-style:translate-y-2\",\n              \"motion-reduce:transition-opacity motion-reduce:data-ending-style:scale-100 motion-reduce:data-starting-style:translate-0 motion-reduce:data-starting-style:scale-100\",\n              previewClassName\n            )}\n          >\n            {image ? (\n              <img\n                data-slot=\"map-pin-image\"\n                src={image.src}\n                alt={image.alt}\n                className=\"block aspect-[16/9] w-full bg-muted object-cover\"\n              />\n            ) : null}\n            {/* The words follow the card a beat behind, so it opens like a\n                card turning over rather than a block appearing. */}\n            <div className=\"flex flex-col gap-1 p-3 transition-[opacity,translate] delay-75 duration-300 ease-out group-data-starting-style/preview:translate-y-1 group-data-starting-style/preview:opacity-0 motion-reduce:transition-none\">\n              {meta !== undefined ? (\n                <span\n                  data-slot=\"map-pin-meta\"\n                  className=\"text-xs text-muted-foreground\"\n                >\n                  {meta}\n                </span>\n              ) : null}\n              {title !== undefined ? (\n                <span\n                  data-slot=\"map-pin-title\"\n                  className=\"text-sm font-medium text-foreground\"\n                >\n                  {title}\n                </span>\n              ) : null}\n              {description !== undefined ? (\n                <p\n                  data-slot=\"map-pin-description\"\n                  className=\"m-0 line-clamp-3 text-sm text-muted-foreground\"\n                >\n                  {description}\n                </p>\n              ) : null}\n              {children !== undefined ? (\n                <div data-slot=\"map-pin-content\" className=\"mt-2\">\n                  {children}\n                </div>\n              ) : null}\n            </div>\n          </PopoverPrimitive.Popup>\n        </PopoverPrimitive.Positioner>\n      </PopoverPrimitive.Portal>\n    </PopoverPrimitive.Root>\n  )\n}\n\nexport { MapPin, mapPinVariants, type MapPinProps }\n",
      "type": "registry:ui"
    }
  ],
  "cssVars": {
    "theme": {
      "color-primary-subtle": "var(--primary-subtle)",
      "color-destructive-foreground": "var(--destructive-foreground)",
      "color-destructive-subtle": "var(--destructive-subtle)",
      "color-success": "var(--success)",
      "color-success-foreground": "var(--success-foreground)",
      "color-success-subtle": "var(--success-subtle)",
      "color-warning": "var(--warning)",
      "color-warning-foreground": "var(--warning-foreground)",
      "color-warning-subtle": "var(--warning-subtle)",
      "color-info": "var(--info)",
      "color-info-foreground": "var(--info-foreground)",
      "color-info-subtle": "var(--info-subtle)",
      "color-ring-subtle": "var(--ring-subtle)",
      "text-color-destructive": "var(--destructive-text)",
      "background-color-destructive": "var(--destructive)",
      "border-color-destructive": "var(--destructive-border)",
      "text-color-success": "var(--success-text)",
      "background-color-success": "var(--success)",
      "border-color-success": "var(--success-border)",
      "text-color-warning": "var(--warning-text)",
      "background-color-warning": "var(--warning)",
      "border-color-warning": "var(--warning-border)",
      "text-color-info": "var(--info-text)",
      "background-color-info": "var(--info)",
      "border-color-info": "var(--info-border)"
    },
    "light": {
      "primary-subtle": "color-mix(in oklch, var(--color-neutral-900) 10%, transparent)",
      "destructive-foreground": "var(--color-neutral-50)",
      "destructive-subtle": "color-mix(in oklch, var(--color-red-700) 8%, transparent)",
      "success": "var(--color-green-700)",
      "success-foreground": "var(--color-neutral-50)",
      "success-subtle": "color-mix(in oklch, var(--color-green-700) 6%, transparent)",
      "warning": "var(--color-amber-700)",
      "warning-foreground": "var(--color-neutral-50)",
      "warning-subtle": "color-mix(in oklch, var(--color-amber-700) 6%, transparent)",
      "info": "var(--color-blue-700)",
      "info-foreground": "var(--color-neutral-50)",
      "info-subtle": "color-mix(in oklch, var(--color-blue-700) 6%, transparent)",
      "ring-subtle": "color-mix(in oklch, var(--color-neutral-400) 50%, transparent)",
      "destructive-text": "var(--destructive)",
      "destructive-border": "var(--destructive)",
      "success-text": "var(--color-green-800)",
      "success-border": "var(--success)",
      "warning-text": "var(--color-amber-800)",
      "warning-border": "var(--warning)",
      "info-text": "var(--info)",
      "info-border": "var(--info)"
    },
    "dark": {
      "primary-subtle": "color-mix(in oklch, var(--color-neutral-50) 20%, transparent)",
      "destructive-foreground": "var(--color-neutral-950)",
      "destructive-subtle": "color-mix(in oklch, var(--color-red-400) 20%, transparent)",
      "success": "var(--color-green-400)",
      "success-foreground": "var(--color-neutral-950)",
      "success-subtle": "color-mix(in oklch, var(--color-green-400) 20%, transparent)",
      "warning": "var(--color-amber-400)",
      "warning-foreground": "var(--color-neutral-950)",
      "warning-subtle": "color-mix(in oklch, var(--color-amber-400) 20%, transparent)",
      "info": "var(--color-blue-400)",
      "info-foreground": "var(--color-neutral-950)",
      "info-subtle": "color-mix(in oklch, var(--color-blue-400) 20%, transparent)",
      "ring-subtle": "color-mix(in oklch, var(--color-neutral-500) 50%, transparent)",
      "destructive-text": "var(--destructive)",
      "destructive-border": "var(--destructive)",
      "success-text": "var(--success)",
      "success-border": "var(--success)",
      "warning-text": "var(--warning)",
      "warning-border": "var(--warning)",
      "info-text": "var(--info)",
      "info-border": "var(--info)"
    }
  },
  "meta": {
    "tier": "special-components",
    "group": "Display",
    "docs": "https://fibo.toribryan.com/?path=/docs/special-components-map-pin--docs"
  },
  "docs": "Docs and live examples: https://fibo.toribryan.com/?path=/docs/special-components-map-pin--docs",
  "categories": [
    "special-components",
    "display"
  ],
  "type": "registry:ui"
}