{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "pull-to-refresh",
  "title": "Pull To Refresh",
  "description": "A scroll area you pull down from the top to reload, with a resisting indicator, a live-region status and an always-reachable Refresh button for keyboard and mouse.",
  "dependencies": [
    "lucide-react"
  ],
  "files": [
    {
      "path": "registry/tealmac/pull-to-refresh.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ArrowDownIcon, Loader2Icon, RefreshCwIcon } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport interface PullToRefreshProps extends Omit<\n  React.ComponentProps<\"div\">,\n  \"onError\"\n> {\n  /** Load fresh data. The indicator stays until the promise settles. */\n  onRefresh: () => Promise<unknown> | void\n  /** Pull distance in px that triggers a refresh. Default 72. */\n  threshold?: number\n  disabled?: boolean\n  /** Text for the three states, announced to screen readers. */\n  labels?: { pull: string; release: string; refreshing: string; done: string }\n  /** Label of the keyboard and mouse button. Pass `null` to remove it. */\n  buttonLabel?: string | null\n}\n\nconst DEFAULT_LABELS = {\n  pull: \"Pull to refresh\",\n  release: \"Release to refresh\",\n  refreshing: \"Refreshing\",\n  done: \"Updated\",\n}\n\n/**\n * A scroll area you can pull down from the top to reload. Touch drives the\n * pull; because a pull cannot be done with a keyboard, a \"Refresh\" button is\n * always reachable (it appears on focus) and runs the same `onRefresh`. The\n * state is announced in a polite live region and the indicator keeps still\n * with reduced motion.\n */\nexport function PullToRefresh({\n  onRefresh,\n  threshold = 72,\n  disabled,\n  labels = DEFAULT_LABELS,\n  buttonLabel = \"Refresh\",\n  className,\n  children,\n  ...props\n}: PullToRefreshProps) {\n  const scrollRef = React.useRef<HTMLDivElement>(null)\n  const [pull, setPull] = React.useState(0)\n  const [phase, setPhase] = React.useState<\n    \"idle\" | \"pulling\" | \"refreshing\" | \"done\"\n  >(\"idle\")\n  const pullRef = React.useRef(0)\n  const busy = React.useRef(false)\n\n  const run = React.useCallback(async () => {\n    if (busy.current) return\n    busy.current = true\n    setPhase(\"refreshing\")\n    setPull(threshold * 0.8)\n    try {\n      await onRefresh()\n    } finally {\n      busy.current = false\n      setPhase(\"done\")\n      setPull(0)\n      window.setTimeout(\n        () => setPhase((p) => (p === \"done\" ? \"idle\" : p)),\n        1200\n      )\n    }\n  }, [onRefresh, threshold])\n\n  React.useEffect(() => {\n    const el = scrollRef.current\n    if (!el || disabled) return\n    let startY = 0\n    let active = false\n    const start = (event: TouchEvent) => {\n      if (busy.current || el.scrollTop > 0) return\n      startY = event.touches[0].clientY\n      active = true\n    }\n    const move = (event: TouchEvent) => {\n      if (!active) return\n      const dy = event.touches[0].clientY - startY\n      if (dy <= 0 || el.scrollTop > 0) {\n        if (pullRef.current) {\n          pullRef.current = 0\n          setPull(0)\n          setPhase(\"idle\")\n        }\n        return\n      }\n      // Diminishing returns past the threshold.\n      const eased = dy < threshold ? dy : threshold + (dy - threshold) * 0.35\n      pullRef.current = eased\n      setPull(eased)\n      setPhase(\"pulling\")\n      if (event.cancelable) event.preventDefault()\n    }\n    const end = () => {\n      if (!active) return\n      active = false\n      if (pullRef.current >= threshold) void run()\n      else setPhase(\"idle\")\n      if (pullRef.current < threshold) setPull(0)\n      pullRef.current = 0\n    }\n    el.addEventListener(\"touchstart\", start, { passive: true })\n    el.addEventListener(\"touchmove\", move, { passive: false })\n    el.addEventListener(\"touchend\", end)\n    el.addEventListener(\"touchcancel\", end)\n    return () => {\n      el.removeEventListener(\"touchstart\", start)\n      el.removeEventListener(\"touchmove\", move)\n      el.removeEventListener(\"touchend\", end)\n      el.removeEventListener(\"touchcancel\", end)\n    }\n  }, [disabled, threshold, run])\n\n  const ready = pull >= threshold\n  const status =\n    phase === \"refreshing\"\n      ? labels.refreshing\n      : phase === \"done\"\n        ? labels.done\n        : phase === \"pulling\"\n          ? ready\n            ? labels.release\n            : labels.pull\n          : \"\"\n\n  return (\n    <div\n      data-slot=\"pull-to-refresh\"\n      data-phase={phase}\n      className={cn(\"relative overflow-hidden\", className)}\n      {...props}\n    >\n      <div role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n        {status}\n      </div>\n      <div\n        aria-hidden\n        className=\"pointer-events-none absolute inset-x-0 top-0 z-10 flex justify-center\"\n        style={{\n          transform: `translateY(${Math.max(0, pull - 40)}px)`,\n          opacity: Math.min(1, pull / threshold),\n        }}\n      >\n        <span className=\"bg-card text-muted-foreground mt-1 grid size-9 place-items-center rounded-full border shadow-md\">\n          {phase === \"refreshing\" ? (\n            <Loader2Icon className=\"size-5 animate-spin motion-reduce:animate-none\" />\n          ) : (\n            <ArrowDownIcon\n              className={cn(\n                \"size-5 transition-transform motion-reduce:transition-none\",\n                ready && \"rotate-180\"\n              )}\n            />\n          )}\n        </span>\n      </div>\n      <div\n        ref={scrollRef}\n        data-slot=\"pull-to-refresh-scroll\"\n        className={cn(\n          \"h-full overflow-y-auto overscroll-y-contain\",\n          phase !== \"pulling\" &&\n            \"transition-transform duration-200 motion-reduce:transition-none\"\n        )}\n        style={{ transform: pull ? `translateY(${pull * 0.6}px)` : undefined }}\n      >\n        {buttonLabel !== null && (\n          <button\n            type=\"button\"\n            onClick={() => void run()}\n            disabled={disabled || phase === \"refreshing\"}\n            className=\"bg-background focus-visible:ring-ring sr-only m-2 inline-flex items-center gap-2 rounded-md border px-3 py-1.5 text-sm focus:not-sr-only focus-visible:ring-2 focus-visible:outline-none\"\n          >\n            <RefreshCwIcon className=\"size-4\" />\n            {buttonLabel}\n          </button>\n        )}\n        {children}\n      </div>\n    </div>\n  )\n}\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}