{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "map-controls",
  "title": "Map Controls",
  "description": "Zoom, compass, locate, and custom camera buttons for any map library.",
  "dependencies": [
    "class-variance-authority",
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "https://uaiblocks.dev/r/uai-theme.json",
    "https://uaiblocks.dev/r/uai-utils.json"
  ],
  "files": [
    {
      "path": "src/registry/uai/components/map-controls.tsx",
      "content": "\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { Compass, LocateFixed, LocateOff, Minus, Plus } from \"lucide-react\";\nimport { type ComponentProps, createContext, type ReactNode, useContext } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/uai-utils\";\n\nexport const MAP_CONTROLS_VARIANTS = [\"stacked\", \"bar\", \"compact\"] as const;\nexport type MapControlsVariant = (typeof MAP_CONTROLS_VARIANTS)[number];\nexport const MAP_CONTROLS_LOCATE_STATUSES = [\"idle\", \"locating\", \"active\", \"error\"] as const;\nexport type MapControlsLocateStatus = (typeof MAP_CONTROLS_LOCATE_STATUSES)[number];\nexport type MapControlsProps = ComponentProps<\"div\"> & { variant?: MapControlsVariant };\n\ntype ControlsContext = { variant: MapControlsVariant };\nconst Context = createContext<ControlsContext | null>(null);\nfunction useControls(part: string) {\n  const context = useContext(Context);\n  if (!context) throw new Error(`${part} must be used within MapControls`);\n  return context;\n}\n\nconst mapControlsVariants = cva(\"flex w-max text-foreground\", {\n  variants: {\n    variant: {\n      stacked: \"flex-col gap-2\",\n      bar: \"flex-row items-center gap-1 rounded-full bg-card p-1 shadow-[0_0_0_1px_var(--border),0_6px_16px_-8px_oklch(0_0_0/0.24)]\",\n      compact: \"flex-col gap-1.5\",\n    },\n  },\n});\n\n/** Groups map camera controls. Each control is a named native button. */\nexport function MapControls({\n  variant = \"stacked\",\n  \"aria-label\": label = \"Map controls\",\n  className,\n  ...props\n}: MapControlsProps) {\n  return (\n    <Context.Provider value={{ variant }}>\n      {/* biome-ignore lint/a11y/useSemanticElements: a labelled group of buttons, not a form fieldset. */}\n      <div\n        data-slot=\"map-controls\"\n        role=\"group\"\n        aria-label={label}\n        className={cn(mapControlsVariants({ variant }), className)}\n        {...props}\n        data-variant={variant}\n      />\n    </Context.Provider>\n  );\n}\n\nconst groupVariants = cva(\"flex\", {\n  variants: {\n    variant: {\n      stacked:\n        \"flex-col overflow-hidden rounded-[14px] bg-card shadow-[0_0_0_1px_var(--border),0_6px_16px_-8px_oklch(0_0_0/0.24)]\",\n      bar: \"flex-row items-center gap-0.5 not-first:border-l not-first:pl-1\",\n      compact:\n        \"flex-col overflow-hidden rounded-xl bg-card shadow-[0_0_0_1px_var(--border),0_4px_12px_-6px_oklch(0_0_0/0.24)]\",\n    },\n  },\n});\n\n/** Visually joins related controls, such as zoom in and zoom out. */\nexport function MapControlsGroup({ className, ...props }: ComponentProps<\"div\">) {\n  const context = useControls(\"MapControlsGroup\");\n  return (\n    <div\n      data-slot=\"map-controls-group\"\n      className={cn(groupVariants({ variant: context.variant }), className)}\n      {...props}\n    />\n  );\n}\n\nconst buttonVariants = cva(\n  \"rounded-none border-0 bg-transparent p-0 text-muted-foreground shadow-none transition-[background-color,color,scale] duration-[120ms,120ms,140ms] ease-[ease-out,ease-out,cubic-bezier(0.23,1,0.32,1)] hover:bg-secondary hover:text-foreground focus-visible:ring-0 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-ring focus-visible:outline-solid active:scale-[0.94] disabled:opacity-40 aria-pressed:text-primary-ink motion-reduce:transition-none motion-reduce:active:scale-100 dark:hover:bg-secondary\",\n  {\n    variants: {\n      variant: {\n        stacked: \"size-8 [&_svg:not([class*='size-'])]:size-4\",\n        bar: \"size-7 rounded-full [&_svg:not([class*='size-'])]:size-[15px]\",\n        compact: \"size-6 [&_svg:not([class*='size-'])]:size-[13px]\",\n      },\n    },\n  },\n);\n\nexport type MapControlsButtonProps = Omit<ComponentProps<\"button\">, \"aria-label\"> & {\n  /** Spoken and tooltip name, for example \"Zoom in\". */\n  label: string;\n};\n\n/** A camera control. Pass an icon as children. */\nexport function MapControlsButton({\n  label,\n  type = \"button\",\n  className,\n  children,\n  ...props\n}: MapControlsButtonProps) {\n  const context = useControls(\"MapControlsButton\");\n  return (\n    <Button\n      data-slot=\"map-controls-button\"\n      type={type}\n      variant=\"ghost\"\n      size=\"icon\"\n      aria-label={label}\n      title={label}\n      className={cn(buttonVariants({ variant: context.variant }), className)}\n      {...props}\n    >\n      {children}\n    </Button>\n  );\n}\n\ntype PresetProps = Omit<MapControlsButtonProps, \"label\" | \"children\"> & {\n  label?: string;\n  children?: ReactNode;\n};\n\nexport function MapControlsZoomIn({ label = \"Zoom in\", children, ...props }: PresetProps) {\n  return (\n    <MapControlsButton label={label} {...props}>\n      {children ?? <Plus strokeWidth={1.75} aria-hidden=\"true\" />}\n    </MapControlsButton>\n  );\n}\n\nexport function MapControlsZoomOut({ label = \"Zoom out\", children, ...props }: PresetProps) {\n  return (\n    <MapControlsButton label={label} {...props}>\n      {children ?? <Minus strokeWidth={1.75} aria-hidden=\"true\" />}\n    </MapControlsButton>\n  );\n}\n\nexport type MapControlsCompassProps = PresetProps & {\n  /** Current map bearing in degrees. The needle counter-rotates to keep pointing north. */\n  bearing?: number;\n};\n\nexport function MapControlsCompass({\n  bearing = 0,\n  label = \"Reset bearing to north\",\n  children,\n  ...props\n}: MapControlsCompassProps) {\n  return (\n    <MapControlsButton label={label} data-bearing={Math.round(bearing)} {...props}>\n      {children ?? (\n        <Compass\n          strokeWidth={1.75}\n          aria-hidden=\"true\"\n          className=\"transition-[rotate] duration-300 ease-out-quint motion-reduce:transition-none\"\n          style={{ rotate: `${-45 - bearing}deg` }}\n        />\n      )}\n    </MapControlsButton>\n  );\n}\n\nconst locateLabels: Record<MapControlsLocateStatus, string> = {\n  idle: \"Show my location\",\n  locating: \"Finding your location\",\n  active: \"Following your location\",\n  error: \"Location unavailable, try again\",\n};\n\nexport type MapControlsLocateProps = PresetProps & { status?: MapControlsLocateStatus };\n\n/** Requests or follows the device location. The status is spoken in the button name. */\nexport function MapControlsLocate({\n  status = \"idle\",\n  label,\n  className,\n  children,\n  ...props\n}: MapControlsLocateProps) {\n  const Icon = status === \"error\" ? LocateOff : LocateFixed;\n  return (\n    <MapControlsButton\n      label={label ?? locateLabels[status]}\n      aria-pressed={status === \"active\"}\n      aria-busy={status === \"locating\" || undefined}\n      data-status={status}\n      className={cn(\n        // The radar ping sits around the icon so grouped controls never clip it.\n        \"relative after:pointer-events-none after:absolute after:top-1/2 after:left-1/2 after:size-2 after:-translate-1/2 after:rounded-full data-[status=error]:text-destructive-ink motion-safe:data-[status=locating]:after:animate-ring-pulse\",\n        className,\n      )}\n      {...props}\n    >\n      {children ?? <Icon strokeWidth={1.75} aria-hidden=\"true\" />}\n    </MapControlsButton>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@ui/uai/map-controls.tsx"
    }
  ],
  "categories": [
    "maps",
    "navigation"
  ],
  "type": "registry:ui"
}