{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "geolocation-button",
  "title": "Geolocation Button",
  "description": "A \"use my current location\" button that is right about why it didn't. It asks the device for a position and, when that is refused, works out which of four unrelated things the refusal actually was — because the browser reports all four with the same number — so what it puts on screen is advice that can work rather than advice that cannot. Reach for it wherever typing an address is the worst part of the page: the \"find my address\" step of a checkout, signup, delivery or booking form; a store, branch, ATM, pharmacy, restaurant or EV-charger finder; a weather, air-quality, pollen, tide or prayer-times view that ought to open on where you already are; the initial centre of a map; a check-in, attendance, inspection or field-service app; a taxi, ride or courier pickup point; local search, classifieds, jobs or property listings; a \"near me\" radius on events or dating; a shipping, delivery-window or tax estimate; and any first run that would otherwise begin by asking somebody which city they are in. Common asks it answers: \"geolocation react\", \"use my current location button\", \"get user location react\", \"navigator.geolocation react\", \"useGeolocation hook\", \"react-geolocated alternative\", \"shadcn geolocation\", \"current location button shadcn\", \"getCurrentPosition react\", \"watchPosition react hook\", \"request location permission react\", \"geolocation permission denied react\", \"user denied geolocation how to ask again\", \"how to re-request location permission\", \"geolocation prompt not showing\", \"getCurrentPosition not working on localhost\", \"geolocation not working over http\", \"geolocation requires https\", \"getCurrentPosition hangs forever\", \"geolocation timeout default infinity\", \"location spinner never stops\", \"clearWatch react\", \"geolocation battery drain\", \"navigator.permissions.query geolocation\", \"check if location is blocked without prompting\", \"geolocation in iframe not working\", \"Permissions-Policy geolocation\". The reason to install one rather than write it is that the platform answers four different questions with one number. The specification's \"request a position\" algorithm calls back with PERMISSION_DENIED when a Permissions Policy forbids the feature, again when the page is not a secure context, and again when the stored permission is \"denied\" — and a prompt the user closes without answering arrives the same way. The version everyone writes maps that code to \"you have blocked location access, turn it back on in your browser settings\", which is true in one of those cases and misleading in the other three. On the http staging box on an internal IP there is no setting to change and no prompt was ever shown. Inside an <iframe> without allow=\"geolocation\", the same. And somebody who merely dismissed the dialog is told they blocked something they did not. So the code is never taken at face value. Two of the causes are settled before calling at all, which is also what stops a doomed request being made: the secure-context check — needed because the attribute is not [SecureContext] and so is present, and useless, on every http origin, which is why the usual feature detect passes there — and document.permissionsPolicy.allowsFeature(\"geolocation\") where a browser has it. The rest is decided by reading the stored state with navigator.permissions.query, which answers without prompting: still \"prompt\" after a refusal means the dialog was dismissed, \"granted\" means the block came from above the user, and \"denied\" means it is the user's own. Only the first of those is offered a retry, because the specification is explicit that once the state is \"denied\", getCurrentPosition calls back immediately without prompting — a \"Try again\" button there cannot work, and returns the same error instantly for as long as the page stays open. What it offers instead is the browser's own site settings, and it subscribes to the PermissionStatus change event, so the moment somebody flips that switch the dead end clears itself and the button works again with no reload. Two more defaults are corrected on the way past. timeout is Infinity in the browser, so the hand-written version hangs — no success, no failure, spinner still turning — on a phone indoors, in a lift or in aeroplane mode; this one always sends a finite deadline. And that deadline does not cover everything, which is why the button separates \"waiting for permission\" from \"finding your location\": by the specification the time spent waiting for the document to become visible and for the permission to be answered is not included in timeout, so an unanswered dialog is an unbounded wait — correctly, because a person deciding whether to hand over their location is not a fault and must not be cut off and told it failed. Watch mode registers with watchPosition and hands it back with clearWatch on unmount, on clear() and before any restart, which is the leak with no symptom on screen: an abandoned watch keeps the location hardware awake for the life of the page, long after the map that wanted it was navigated away from. Official shadcn/ui has nothing here — geolocation, getCurrentPosition, watchPosition, clearWatch, coords, navigator.permissions and PermissionStatus appear nowhere in its components. Within pulld it is the one that has to ask for something: network-status works out whether the network is really there, wake-lock-toggle holds a resource the browser can take back silently, and this one handles the permission that can be refused for good. It sits naturally beside a map or an address form, and alongside country-select, phone-input or timezone-select on the same form. useGeolocation() is exported for a control of your own, handing back phase, position, failure, permission, isSupported, request() and clear(); isGeolocationSupported() is exported too and reports only that the API exists — deliberately not that it will work, which on http is a different question. The failure object carries a cause, the browser's own code, a retryable flag and wording you can override per cause, so a design of your own can draw the same distinctions. The message sits in an always-mounted polite live region, because a live region inserted together with its text is not reliably announced and would be silent for exactly the people relying on it. The button uses aria-disabled rather than disabled, so a refusal keeps its place in the tab order and can still explain itself, points at the message with aria-describedby, and carries aria-busy while it waits. Every colour is a shadcn token, so it follows light and dark, and the whole thing is one file.",
  "dependencies": [
    "lucide-react"
  ],
  "files": [
    {
      "path": "registry/ui/geolocation-button.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Loader2, LocateFixed, TriangleAlert } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/**\n * `GeolocationPositionError.code` values, written out rather than read off the instance.\n *\n * The constants live on the error object the browser hands you, so `err.PERMISSION_DENIED` works —\n * but only when the thing you are holding really is a `GeolocationPositionError`. Anything that\n * hands this component a plain object shaped like one (a test, a polyfill, a wrapper that\n * serialised the error across a boundary) has the code and not the constants, and a comparison\n * against `err.PERMISSION_DENIED` then compares `1` with `undefined` and silently takes the wrong\n * branch. The numbers are fixed by the specification and are never going to move.\n */\nconst PERMISSION_DENIED = 1\nconst POSITION_UNAVAILABLE = 2\nconst TIMEOUT = 3\n\n/**\n * The Geolocation entry point, or null where there isn't one.\n *\n * Hand-written because TypeScript declares `readonly geolocation: Geolocation` on `Navigator` —\n * not optional — so `navigator.geolocation.getCurrentPosition(...)` type-checks and then throws\n * a TypeError on anything that hasn't got it.\n */\nfunction geolocationOf(): Geolocation | null {\n  if (typeof navigator === \"undefined\" || !(\"geolocation\" in navigator)) return null\n  const api: Geolocation | undefined = navigator.geolocation\n  return api && typeof api.getCurrentPosition === \"function\" ? api : null\n}\n\n/**\n * Whether the API exists here at all.\n *\n * Deliberately *not* a check for whether it will work. Read the note on `insecureContext` below:\n * this returning true is compatible with every single call failing, which is the whole reason the\n * usual feature-detect is not enough.\n */\nexport function isGeolocationSupported(): boolean {\n  return geolocationOf() !== null\n}\n\n/**\n * Whether this page is a non-secure context, where the API is present and permanently broken.\n *\n * This is the one that costs an afternoon. Geolocation is gated on secure contexts, so the\n * reasonable assumption is that `navigator.geolocation` is simply absent over plain http and a\n * feature-detect catches it. It is not absent. The specification does not mark the attribute\n * `[SecureContext]` — the IDL is a plain `readonly attribute Geolocation geolocation` — and the\n * gate lives inside the algorithm instead: \"request a position\" checks for a non-secure context\n * and calls back with **PERMISSION_DENIED**.\n *\n * So on the http staging box on an internal IP, the object is there, the feature-detect passes,\n * the call is made, no prompt ever appears, and what comes back is the same code a user gets for\n * pressing Block. A component that maps that code to \"you have blocked location access, change it\n * in your browser settings\" sends everyone hunting through a settings screen for a switch that is\n * not the problem and cannot fix it. Checked before calling so the message can say the true thing.\n */\nfunction insecureContext(): boolean {\n  // `isSecureContext` is itself missing on old enough browsers, and \"missing\" must not read as\n  // \"insecure\" — that would refuse to work on the very browsers this is meant to degrade for.\n  return typeof window !== \"undefined\" && window.isSecureContext === false\n}\n\n/**\n * Whether Permissions Policy forbids geolocation in this document, where that can be asked.\n *\n * The second producer of an unexplained PERMISSION_DENIED, and it sits one step above the\n * secure-context check in the same algorithm. The default allowlist for the feature is `'self'`,\n * so an `<iframe>` embedding this page without `allow=\"geolocation\"` is refused, as is any origin\n * serving `Permissions-Policy: geolocation=()`. Neither is something the person looking at the\n * screen can do anything about, and neither shows a prompt.\n *\n * The accessor is not in the DOM typings and is absent in some browsers, so this is best-effort:\n * false means \"not known to be blocked\", never \"allowed\".\n */\nfunction blockedByPermissionsPolicy(): boolean {\n  if (typeof document === \"undefined\") return false\n  type Policy = { allowsFeature?: (feature: string) => boolean }\n  const doc = document as Document & { permissionsPolicy?: Policy; featurePolicy?: Policy }\n  const policy = doc.permissionsPolicy ?? doc.featurePolicy\n  if (!policy || typeof policy.allowsFeature !== \"function\") return false\n  try {\n    return !policy.allowsFeature(\"geolocation\")\n  } catch {\n    return false\n  }\n}\n\n/**\n * Reads the stored permission without prompting, or null where that cannot be asked.\n *\n * This is the lever the whole component turns on. `getCurrentPosition` is the only other way to\n * learn the permission state and it *acts* — it can put a prompt in front of someone who never\n * asked for one. `permissions.query` answers the same question silently, which is what makes it\n * possible to tell the four different things PERMISSION_DENIED means apart.\n *\n * Guarded twice over. `navigator.permissions` is declared non-optional by TypeScript and is\n * genuinely absent in older browsers, and a browser that *has* the Permissions API may still not\n * recognise this particular descriptor, in which case `query` rejects with a TypeError instead of\n * answering. The API also arrived late here — Safari only shipped `query` in 16 — so not knowing\n * is an ordinary answer rather than an error, and every path below has to work without it.\n */\nasync function queryGeolocationPermission(): Promise<PermissionStatus | null> {\n  if (typeof navigator === \"undefined\" || !(\"permissions\" in navigator)) return null\n  const permissions: Permissions | undefined = navigator.permissions\n  if (!permissions || typeof permissions.query !== \"function\") return null\n  try {\n    return await permissions.query({ name: \"geolocation\" })\n  } catch {\n    return null\n  }\n}\n\n/**\n * Why a request failed — worked out, not guessed.\n *\n * Four of these arrive as the identical `PERMISSION_DENIED`, and they need four different things\n * from four different people: `denied` is the user's own stored choice, `dismissed` is a prompt\n * closed without an answer, `insecure-context` and `blocked-by-policy` are the developer's to fix\n * and are invisible to the user.\n */\nexport type GeolocationFailureCause =\n  | \"unsupported\"\n  | \"insecure-context\"\n  | \"blocked-by-policy\"\n  | \"denied\"\n  | \"dismissed\"\n  | \"unavailable\"\n  | \"timeout\"\n\nexport interface GeolocationFailure {\n  cause: GeolocationFailureCause\n  /** The browser's own code, or null when this was settled before any call was made. */\n  code: number | null\n  /** Wording safe to show as-is. Override per cause with the `messages` prop. */\n  message: string\n  /**\n   * Whether asking again, right now, can produce a different answer.\n   *\n   * False for `denied` is the point of the component. Once the stored state is \"denied\" the\n   * specification has `getCurrentPosition` call back with PERMISSION_DENIED **without prompting**,\n   * so a \"Try again\" button is a button that cannot work: it will return the same error, instantly,\n   * for as long as the page is open. The only way out runs through the browser's own site settings,\n   * which is why the copy for that case points there — and why the permission `change` listener\n   * below exists, so that coming back from those settings costs nothing.\n   */\n  retryable: boolean\n}\n\n/** Where a request has got to. `prompting` is the browser's permission dialog being answered. */\nexport type GeolocationPhase = \"idle\" | \"prompting\" | \"locating\" | \"success\" | \"error\"\n\nconst RETRYABLE: Record<GeolocationFailureCause, boolean> = {\n  unsupported: false,\n  \"insecure-context\": false,\n  \"blocked-by-policy\": false,\n  denied: false,\n  dismissed: true,\n  unavailable: true,\n  timeout: true,\n}\n\nconst DEFAULT_MESSAGES: Record<GeolocationFailureCause, string> = {\n  unsupported: \"This browser can't share your location.\",\n  \"insecure-context\":\n    \"Location needs a secure (https) connection, so the browser refused without asking.\",\n  \"blocked-by-policy\": \"This page isn't permitted to use location.\",\n  denied:\n    \"Location is blocked for this site. Allow it in your browser's site settings — this page can't ask again.\",\n  dismissed: \"The location request was dismissed.\",\n  unavailable: \"Your location couldn't be determined.\",\n  timeout: \"Finding your location took too long.\",\n}\n\nexport interface UseGeolocationOptions {\n  /** Ask the device for its best fix. Costs battery and time; off by default. */\n  enableHighAccuracy?: boolean\n  /**\n   * How long the device may spend acquiring a fix, in milliseconds.\n   *\n   * Defaulted, and never left alone, because **the browser's own default is `Infinity`**. Omit it\n   * and a phone indoors, in a lift, in a basement or in aeroplane mode neither succeeds nor fails:\n   * it hangs, and the spinner is still turning when the user gives up. Note what it does *not*\n   * cover — see `phase`.\n   */\n  timeout?: number\n  /**\n   * How old a cached fix may be, in milliseconds. 0 (the browser's default) always measures afresh.\n   *\n   * Worth raising for an address form or a store finder, where a reading from a minute ago is the\n   * same reading and arrives instantly instead of waking the GPS. One catch: the cache is only\n   * consulted when this is greater than 0, and a cached fix is only reused when its accuracy mode\n   * matches, so flipping `enableHighAccuracy` throws away what was cached.\n   */\n  maximumAge?: number\n  /**\n   * Follow the device instead of taking one reading.\n   *\n   * The watch is registered with `watchPosition` and handed back with `clearWatch` on unmount, on\n   * `clear()`, and before any restart. Skipping that is the component's other quiet cost: an\n   * abandoned watch keeps the location hardware awake for the life of the page — a battery drain\n   * with no symptom on screen, since the map that wanted it has long since been navigated away\n   * from.\n   */\n  watch?: boolean\n  /** Called with each fix. */\n  onPosition?: (position: GeolocationPosition) => void\n  /**\n   * Called when a request fails.\n   *\n   * Not `onError`: that is a native DOM attribute React defines on every element, so a prop by that\n   * name collides with it as soon as these options are spread onto the `<button>`.\n   */\n  onFailure?: (failure: GeolocationFailure) => void\n}\n\nexport interface UseGeolocationResult {\n  /**\n   * Where the request has got to.\n   *\n   * `prompting` and `locating` are split because the browser treats them differently and saying so\n   * is the difference between a truthful spinner and a stuck one. `timeout` covers acquisition\n   * only: by the specification, \"the time spent waiting for the document to become visible and for\n   * obtaining permission to use the API is not included\". A permission dialog sitting unanswered is\n   * therefore unbounded however small a timeout is passed — correctly so, since someone deciding\n   * whether to hand over their location is not a fault and must not be cut off and reported as\n   * one. What that leaves is a spinner obliged to say which wait it is: `prompting` means the ball\n   * is in the user's court, `locating` means it is the device's.\n   */\n  phase: GeolocationPhase\n  /** The most recent fix, or null. Kept across a later failure. */\n  position: GeolocationPosition | null\n  /** The last failure, or null. Cleared when a request starts or succeeds. */\n  failure: GeolocationFailure | null\n  /** The stored permission, or \"unknown\" where the Permissions API can't say. */\n  permission: PermissionState | \"unknown\"\n  /** Whether the API exists. Starts true so the server and first client render agree. */\n  isSupported: boolean\n  /** Ask for a position. Ignored while one is already in flight. */\n  request: () => void\n  /** Drop the fix and any error, release a watch, and go back to idle. */\n  clear: () => void\n}\n\n/**\n * The whole behaviour, for a control you lay out yourself.\n */\nexport function useGeolocation({\n  enableHighAccuracy = false,\n  timeout = 10_000,\n  maximumAge = 0,\n  watch = false,\n  onPosition,\n  onFailure,\n}: UseGeolocationOptions = {}): UseGeolocationResult {\n  const [phase, setPhase] = React.useState<GeolocationPhase>(\"idle\")\n  const [position, setPosition] = React.useState<GeolocationPosition | null>(null)\n  const [failure, setFailure] = React.useState<GeolocationFailure | null>(null)\n  const [permission, setPermission] = React.useState<PermissionState | \"unknown\">(\"unknown\")\n  const [isSupported, setIsSupported] = React.useState(true)\n\n  const watchIdRef = React.useRef<number | null>(null)\n  // Every request gets a number, and a callback that is not the current one is dropped. Results\n  // arrive asynchronously and there is no way to cancel a `getCurrentPosition` already in flight,\n  // so without this a stale fix from an abandoned attempt can land on top of a fresh error.\n  const requestRef = React.useRef(0)\n  const inFlightRef = React.useRef(false)\n  const mountedRef = React.useRef(true)\n  const permissionRef = React.useRef<PermissionState | \"unknown\">(\"unknown\")\n  const failureRef = React.useRef<GeolocationFailure | null>(null)\n\n  const onPositionRef = React.useRef(onPosition)\n  const onFailureRef = React.useRef(onFailure)\n\n  // Declared before everything that reads it: under StrictMode the cleanups run and the effects run\n  // again, and if this came last the second pass would see `false` and quietly refuse to work.\n  React.useEffect(() => {\n    mountedRef.current = true\n    return () => {\n      mountedRef.current = false\n    }\n  }, [])\n\n  React.useEffect(() => {\n    onPositionRef.current = onPosition\n  }, [onPosition])\n\n  React.useEffect(() => {\n    onFailureRef.current = onFailure\n  }, [onFailure])\n\n  React.useEffect(() => {\n    permissionRef.current = permission\n  }, [permission])\n\n  React.useEffect(() => {\n    failureRef.current = failure\n  }, [failure])\n\n  React.useEffect(() => {\n    setIsSupported(isGeolocationSupported())\n  }, [])\n\n  const stopWatch = React.useCallback(() => {\n    const id = watchIdRef.current\n    if (id === null) return\n    watchIdRef.current = null\n    geolocationOf()?.clearWatch(id)\n  }, [])\n\n  const fail = React.useCallback((cause: GeolocationFailureCause, code: number | null) => {\n    const next: GeolocationFailure = {\n      cause,\n      code,\n      message: DEFAULT_MESSAGES[cause],\n      retryable: RETRYABLE[cause],\n    }\n    failureRef.current = next\n    setFailure(next)\n    setPhase(\"error\")\n    onFailureRef.current?.(next)\n  }, [])\n\n  /**\n   * Reads the current permission and keeps the dead end honest.\n   *\n   * The `change` event is the reason this is a subscription rather than one read. A user sent to\n   * site settings by the `denied` message comes back to a page that is still open, and the browser\n   * fires `change` on the status object the moment they flip the switch. Handling it is what turns\n   * \"allow it in your settings\" into advice that visibly works; ignoring it leaves the button dead\n   * until a reload, which is the point at which people conclude the site is broken and leave.\n   */\n  React.useEffect(() => {\n    let status: PermissionStatus | null = null\n    let cancelled = false\n\n    const handleChange = () => {\n      if (!status) return\n      const next = status.state\n      setPermission(next)\n      permissionRef.current = next\n      if (next === \"denied\") return\n      // No longer blocked. A `denied` message on screen is now false, and leaving it there is worse\n      // than never having shown it. Only that one is cleared: a timeout or an unavailable position\n      // has nothing to do with permission and is still true.\n      if (failureRef.current?.cause === \"denied\") {\n        failureRef.current = null\n        setFailure(null)\n        setPhase(\"idle\")\n      }\n    }\n\n    void queryGeolocationPermission().then((result) => {\n      if (cancelled || !result) return\n      status = result\n      setPermission(result.state)\n      permissionRef.current = result.state\n      result.addEventListener(\"change\", handleChange)\n    })\n\n    return () => {\n      cancelled = true\n      status?.removeEventListener(\"change\", handleChange)\n    }\n  }, [])\n\n  // Handing the watch back is not optional and nothing else will do it: the registration belongs to\n  // the browser, not to this component, so navigating away inside a single-page app leaves it\n  // running with no control left that could stop it.\n  React.useEffect(() => stopWatch, [stopWatch])\n\n  const request = React.useCallback(() => {\n    if (inFlightRef.current) return\n\n    const api = geolocationOf()\n    if (!api) {\n      setIsSupported(false)\n      fail(\"unsupported\", null)\n      return\n    }\n    // Both of these produce PERMISSION_DENIED with no prompt and no way for the user to help.\n    // Settled here, before calling, so the reason survives instead of being flattened into a code\n    // that means four things.\n    if (insecureContext()) {\n      fail(\"insecure-context\", null)\n      return\n    }\n    if (blockedByPermissionsPolicy()) {\n      fail(\"blocked-by-policy\", null)\n      return\n    }\n\n    const id = requestRef.current + 1\n    requestRef.current = id\n    inFlightRef.current = true\n    failureRef.current = null\n    setFailure(null)\n    // \"prompt\" is the one state that reliably means a dialog is about to appear. Where the\n    // Permissions API could not answer, claiming to know would be the lie, so it says \"locating\".\n    setPhase(permissionRef.current === \"prompt\" ? \"prompting\" : \"locating\")\n\n    const current = () => mountedRef.current && requestRef.current === id\n\n    const handlePosition = (next: GeolocationPosition) => {\n      inFlightRef.current = false\n      if (!current()) return\n      setPosition(next)\n      failureRef.current = null\n      setFailure(null)\n      setPhase(\"success\")\n      onPositionRef.current?.(next)\n    }\n\n    const handleError = (error: GeolocationPositionError) => {\n      inFlightRef.current = false\n      if (!current()) return\n      if (error.code === POSITION_UNAVAILABLE) return fail(\"unavailable\", error.code)\n      if (error.code === TIMEOUT) return fail(\"timeout\", error.code)\n\n      // PERMISSION_DENIED, which is four different things.\n      //\n      // The two checked before the call are checked again, because a document can be moved into a\n      // frame that forbids the feature between the two moments, and because a browser reaching this\n      // code path when the pre-checks could not run (no `allowsFeature`) still deserves the right\n      // answer. Then the stored state decides the rest, and it has to be read now rather than taken\n      // from React state: pressing Block both fails this call and fires `change`, and there is no\n      // guarantee the event has been delivered yet.\n      if (insecureContext()) return fail(\"insecure-context\", error.code)\n      if (blockedByPermissionsPolicy()) return fail(\"blocked-by-policy\", error.code)\n\n      void queryGeolocationPermission().then((status) => {\n        if (!current()) return\n        if (!status) {\n          // Nothing can tell these apart here, so the copy for `denied` carries the day — it is the\n          // only one of the two that stays broken, and advice to check site settings is harmless to\n          // somebody who merely closed the dialog.\n          return fail(\"denied\", error.code)\n        }\n        setPermission(status.state)\n        permissionRef.current = status.state\n        if (status.state === \"denied\") return fail(\"denied\", error.code)\n        // Refused while the stored answer is \"granted\" can only come from above the user — a frame\n        // or a header — so sending them to their own settings would be wrong.\n        if (status.state === \"granted\") return fail(\"blocked-by-policy\", error.code)\n        // Still \"prompt\": nothing was stored, so the dialog was closed rather than answered. This\n        // is the one denial worth offering a retry for, and asking again really does re-prompt.\n        fail(\"dismissed\", error.code)\n      })\n    }\n\n    const options: PositionOptions = { enableHighAccuracy, timeout, maximumAge }\n\n    if (watch) {\n      stopWatch()\n      watchIdRef.current = api.watchPosition(handlePosition, handleError, options)\n      // A watch stays open and keeps reporting, so \"in flight\" ends once it is registered —\n      // otherwise the first fix would be the only one this component ever accepted.\n      inFlightRef.current = false\n    } else {\n      api.getCurrentPosition(handlePosition, handleError, options)\n    }\n  }, [enableHighAccuracy, fail, maximumAge, stopWatch, timeout, watch])\n\n  const clear = React.useCallback(() => {\n    // Bumping the id first orphans anything still in flight, so a fix that lands after this does\n    // not quietly undo it.\n    requestRef.current += 1\n    inFlightRef.current = false\n    stopWatch()\n    setPosition(null)\n    failureRef.current = null\n    setFailure(null)\n    setPhase(\"idle\")\n  }, [stopWatch])\n\n  return { phase, position, failure, permission, isSupported, request, clear }\n}\n\nexport interface GeolocationButtonProps\n  extends Omit<React.ComponentPropsWithoutRef<\"button\">, \"children\">,\n    UseGeolocationOptions {\n  /** Resting label. */\n  label?: string\n  /** While the browser's permission dialog is open. */\n  promptingLabel?: string\n  /** While the device is working out where it is. */\n  locatingLabel?: string\n  /** After a fix arrives. */\n  successLabel?: string\n  /** Offered only where asking again can actually change the answer. */\n  retryLabel?: string\n  /** Drop the visible text and keep it as the accessible name. */\n  iconOnly?: boolean\n  /** Hide the message under the button. It stays in the live region either way. */\n  hideMessage?: boolean\n  /** Per-cause wording, merged over the defaults. */\n  messages?: Partial<Record<GeolocationFailureCause, string>>\n  /** Class for the wrapper. `className` goes to the button. */\n  containerClassName?: string\n}\n\n/**\n * A \"use my current location\" button that tells the truth about why it didn't.\n */\nexport function GeolocationButton({\n  label = \"Use my location\",\n  promptingLabel = \"Waiting for permission…\",\n  locatingLabel = \"Finding your location…\",\n  successLabel = \"Location found\",\n  retryLabel = \"Try again\",\n  iconOnly = false,\n  hideMessage = false,\n  messages,\n  containerClassName,\n  enableHighAccuracy,\n  timeout,\n  maximumAge,\n  watch,\n  onPosition,\n  onFailure,\n  onClick,\n  className,\n  ...props\n}: GeolocationButtonProps) {\n  const { phase, failure, isSupported, request } = useGeolocation({\n    enableHighAccuracy,\n    timeout,\n    maximumAge,\n    watch,\n    onPosition,\n    onFailure,\n  })\n\n  // The message is referenced by id from the button, so it needs one that survives hydration.\n  const messageId = React.useId()\n\n  const busy = phase === \"prompting\" || phase === \"locating\"\n  // The only two states where pressing it again is a real offer. A failure marked non-retryable\n  // keeps the control reachable and says why, rather than dangling an action that cannot work.\n  const actionable = isSupported && !busy && failure?.retryable !== false\n\n  function handleClick(event: React.MouseEvent<HTMLButtonElement>) {\n    onClick?.(event)\n    if (event.defaultPrevented || !actionable) return\n    request()\n  }\n\n  const message = failure ? (messages?.[failure.cause] ?? failure.message) : \"\"\n\n  const visibleLabel =\n    phase === \"prompting\"\n      ? promptingLabel\n      : phase === \"locating\"\n        ? locatingLabel\n        : phase === \"success\"\n          ? successLabel\n          : failure?.retryable\n            ? retryLabel\n            : label\n\n  const Icon = busy ? Loader2 : failure && !failure.retryable ? TriangleAlert : LocateFixed\n\n  return (\n    <div className={cn(\"flex flex-col items-start gap-2\", containerClassName)}>\n      <button\n        type=\"button\"\n        onClick={handleClick}\n        // `aria-disabled` rather than `disabled`, so the control keeps its place in the tab order\n        // and can still be reached and read. A real `disabled` button is skipped entirely, which\n        // means the one explanation of why location is unavailable is delivered to everyone except\n        // the people who most need it. The click handler above is what refuses.\n        aria-disabled={actionable ? undefined : true}\n        aria-busy={busy || undefined}\n        aria-label={iconOnly ? visibleLabel : undefined}\n        // Points at the message so the reason is part of the button's description wherever one\n        // exists, rather than only being announced once as it appears.\n        aria-describedby={message ? messageId : undefined}\n        data-phase={phase}\n        data-cause={failure?.cause}\n        className={cn(\n          \"inline-flex h-9 items-center justify-center gap-2 rounded-md border border-input bg-transparent text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring aria-disabled:pointer-events-none aria-disabled:opacity-50\",\n          iconOnly ? \"w-9\" : \"px-4\",\n          className\n        )}\n        {...props}\n      >\n        <Icon className={cn(\"h-4 w-4\", busy && \"animate-spin\")} aria-hidden=\"true\" />\n        {iconOnly ? null : visibleLabel}\n      </button>\n      {/*\n        Mounted from the start and left empty, never conditionally rendered. A live region that is\n        inserted into the document already holding its text is not reliably announced — the region\n        has to exist for the browser to notice the text changing inside it — so the version that\n        only appears when something goes wrong is silent for exactly the users depending on it.\n      */}\n      <p\n        id={messageId}\n        role=\"status\"\n        aria-live=\"polite\"\n        className={cn(\n          \"text-sm text-muted-foreground\",\n          (hideMessage || !message) && \"sr-only\"\n        )}\n      >\n        {message}\n      </p>\n    </div>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui",
  "docs": "Install any pulld component by name: add \"@pulld\": \"https://pulld.pages.dev/r/{name}.json\" to the registries block in components.json, then `npx shadcn add @pulld/<name>`. All 103 components: https://pulld.pages.dev/?utm_source=cli"
}
