{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "load-more",
  "type": "registry:ui",
  "title": "Load More",
  "description": "A self-replacing pagination trigger. Click a button or scroll a sentinel into view; htmx appends the next page and swaps in a fresh trigger via hx-swap=\"outerHTML\". When the server omits the trigger, the chain ends. The click mode is a real <button> so it works without JavaScript.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/ui/load-more.tsx",
      "type": "registry:ui",
      "target": "components/ui/load-more.tsx",
      "content": "/** @jsxImportSource hono/jsx */\nimport type { PropsWithChildren } from \"hono/jsx\"\nimport { cn, type ClassValue } from \"@/registry/lib/cn\"\n\n// Load More — shadcn-htmx, htmx v4 + Tailwind v4.\n//\n// A self-replacing pagination trigger. It appends the next page and swaps a\n// fresh trigger in its place; when the server omits the trigger, the chain\n// ends. Two modes:\n//   - \"click\"    → a real <button>. Works with no JS (it's a plain button);\n//                  htmx upgrades the click into a request.\n//   - \"intersect\"/\"revealed\" → a scroll sentinel that fires when it enters\n//                  the viewport (IntersectionObserver under the hood).\n//\n// shadcn/ui has no \"load more\" widget (it's a hypermedia loading pattern, not\n// a Radix primitive), so there is no React source of truth to mirror. We build\n// it straight from the htmx v4 loading patterns and the platform docs:\n//   repos/htmx/www/src/content/patterns/01-loading/01-click-to-load.md\n//     (button + hx-swap=\"outerHTML\" + hx-target=\"this\" → self-replace)\n//   repos/htmx/www/src/content/patterns/01-loading/02-infinite-scroll.md\n//     (sentinel + hx-trigger=\"revealed\" / \"intersect once\")\n//   repos/htmx/www/src/content/reference/01-attributes/06-hx-trigger.md\n//     (verified v4: synthetic `revealed` and `intersect` events; `intersect`\n//      supports root:<sel> and threshold:<float>; use `intersect once` inside\n//      an overflow-y:scroll container, `revealed` for the page viewport)\n//   repos/htmx/www/src/content/reference/01-attributes/07-hx-swap.md\n//     (verified v4: `outerHTML` replaces the target element wholesale)\n//   repos/htmx/www/src/content/reference/01-attributes/19-hx-indicator.md\n//     (the htmx-request class rides on this element while in flight; the\n//      .htmx-indicator child is revealed → our skeleton/spinner fallback)\n//   repos/mdn/files/en-us/web/api/intersection_observer_api/index.md\n//     (the platform API htmx's intersect/revealed triggers are built on —\n//      \"implementing infinite-scrolling websites … as you scroll\")\n//\n// Why a real <button> for the click mode: progressive enhancement. Without JS\n// the button still submits (wrap it in a <form action> if you need a true\n// no-JS navigation); with htmx it self-replaces in place. No emulation of any\n// platform feature — the trigger IS the platform's button / IntersectionObserver.\n//\n// Zero JS of our own: htmx owns the request lifecycle and the IntersectionObserver.\n// The in-flight skeleton is pure CSS via the .htmx-indicator opacity contract.\n\nexport type LoadMoreTrigger = \"click\" | \"intersect\" | \"revealed\"\n\n// Click mode renders a button styled like the ghost Button (so it reads as a\n// quiet, full-width \"show more\" affordance); the sentinel modes render a\n// muted, centred status strip like the feed sentinel.\nconst buttonClasses =\n  \"inline-flex w-full shrink-0 items-center justify-center gap-2 rounded-md px-4 py-2 text-sm font-medium whitespace-nowrap outline-none transition-all \" +\n  \"text-foreground hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50 \" +\n  \"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 \" +\n  \"disabled:pointer-events-none disabled:opacity-50 \" +\n  // While the request this button triggers is in flight, htmx adds\n  // .htmx-request here; we mute the trigger so it can't be re-fired.\n  \"[&.htmx-request]:pointer-events-none [&.htmx-request]:opacity-70\"\n\nconst sentinelClasses =\n  \"flex items-center justify-center gap-2 py-4 text-sm text-muted-foreground\"\n\n// The default inline spinner. It carries the htmx-indicator class so it is\n// hidden until htmx flips on .htmx-request on the trigger, then fades in.\nfunction Spinner() {\n  return (\n    <span\n      class=\"htmx-indicator size-4 animate-spin rounded-full border-2 border-muted-foreground/30 border-t-muted-foreground\"\n      aria-hidden=\"true\"\n    />\n  )\n}\n\ntype LoadMoreProps = PropsWithChildren<{\n  // Next-page URL. Sets hx-get for you.\n  href?: string\n  // \"click\" → <button>; \"intersect\"/\"revealed\" → scroll sentinel <div>.\n  trigger?: LoadMoreTrigger\n  // Visible label for the click button (ignored by sentinel modes, which use\n  // their children / default spinner).\n  label?: string\n  // Accessible name. On the sentinel modes the visible text is decorative, so\n  // an explicit label keeps AT announcements meaningful.\n  ariaLabel?: string\n  // Disable the click trigger (no effect on sentinel modes).\n  disabled?: boolean\n  class?: ClassValue\n  id?: string\n  // htmx / data / aria attributes ride onto the trigger element. Forwarded so\n  // call sites can override hx-target, add hx-indicator, hx-vals, etc.\n  [key: `hx-${string}`]: any\n  [key: `data-${string}`]: any\n  [key: `aria-${string}`]: any\n}>\n\nexport function LoadMore(props: LoadMoreProps) {\n  const {\n    href,\n    trigger = \"click\",\n    label = \"Load more\",\n    ariaLabel,\n    disabled,\n    class: className,\n    id,\n    children,\n    ...rest\n  } = props as any\n\n  // Sentinel modes: revealed (page viewport) or intersect (overflow container).\n  if (trigger === \"intersect\" || trigger === \"revealed\") {\n    const hxTrigger = trigger === \"intersect\" ? \"intersect once\" : \"revealed\"\n    return (\n      <div\n        id={id}\n        data-slot=\"load-more\"\n        data-trigger={trigger}\n        role=\"status\"\n        aria-label={ariaLabel ?? \"Loading more\"}\n        hx-get={href}\n        hx-trigger={hxTrigger}\n        hx-swap=\"outerHTML\"\n        class={cn(sentinelClasses, className)}\n        {...rest}\n      >\n        {children ?? (\n          <>\n            <Spinner />\n            Loading more…\n          </>\n        )}\n      </div>\n    )\n  }\n\n  // Click mode: a real button that self-replaces. outerHTML + target=this so\n  // the response (next items + a fresh trigger) takes this element's place.\n  return (\n    <button\n      type=\"button\"\n      id={id}\n      data-slot=\"load-more\"\n      data-trigger=\"click\"\n      disabled={disabled}\n      aria-label={ariaLabel}\n      hx-get={href}\n      hx-trigger=\"click\"\n      hx-target=\"this\"\n      hx-swap=\"outerHTML\"\n      class={cn(buttonClasses, className)}\n      {...rest}\n    >\n      <Spinner />\n      {children ?? label}\n    </button>\n  )\n}\n"
    },
    {
      "path": "registry/jinja2/load-more.html",
      "type": "registry:file",
      "target": "templates/components/load-more.html",
      "content": "{# Load More macro — shadcn-htmx, htmx v4 + Tailwind v4.\n   Mirrors registry/ui/load-more.tsx.\n\n   A self-replacing pagination trigger. It appends the next page and swaps a\n   fresh trigger into its place (hx-swap=\"outerHTML\"); when the server omits\n   the trigger, the chain ends. trigger=\"click\" renders a real <button>\n   (works with no JS); trigger=\"intersect\"/\"revealed\" renders a scroll\n   sentinel (htmx infinite scroll, IntersectionObserver under the hood).\n\n   Sources cited in load-more.tsx:\n     repos/htmx/.../patterns/01-loading/01-click-to-load.md\n     repos/htmx/.../patterns/01-loading/02-infinite-scroll.md\n     repos/htmx/.../reference/01-attributes/{06-hx-trigger,07-hx-swap,19-hx-indicator}.md\n     repos/mdn/.../web/api/intersection_observer_api/index.md\n\n   Usage:\n     {% from \"components/load-more.html\" import load_more %}\n\n     {{ load_more(href=\"/comments?page=2\", label=\"Show more comments\") }}\n     {{ load_more(href=\"/contacts?page=2\", trigger=\"intersect\") }} #}\n\n{% macro load_more(href=none, trigger=\"click\", label=\"Load more\", aria_label=none, disabled=false, id=none, extra_class=\"\", **attrs) %}\n{% if trigger in (\"intersect\", \"revealed\") %}\n<div\n  {%- if id %} id=\"{{ id }}\"{% endif %}\n  data-slot=\"load-more\" data-trigger=\"{{ trigger }}\"\n  role=\"status\" aria-label=\"{{ aria_label or 'Loading more' }}\"\n  {% if href %}hx-get=\"{{ href }}\"{% endif %}\n  hx-trigger=\"{{ 'intersect once' if trigger == 'intersect' else 'revealed' }}\" hx-swap=\"outerHTML\"\n  class=\"flex items-center justify-center gap-2 py-4 text-sm text-muted-foreground {{ extra_class }}\"\n  {%- for k, v in attrs.items() %} {{ k|replace('_', '-') }}=\"{{ v }}\"{% endfor -%}\n>{% if caller %}{{ caller() }}{% else %}<span class=\"htmx-indicator size-4 animate-spin rounded-full border-2 border-muted-foreground/30 border-t-muted-foreground\" aria-hidden=\"true\"></span>Loading more…{% endif %}</div>\n{% else %}\n<button type=\"button\"\n  {%- if id %} id=\"{{ id }}\"{% endif %}\n  data-slot=\"load-more\" data-trigger=\"click\"\n  {% if disabled %}disabled{% endif %}\n  {% if aria_label %}aria-label=\"{{ aria_label }}\"{% endif %}\n  {% if href %}hx-get=\"{{ href }}\"{% endif %}\n  hx-trigger=\"click\" hx-target=\"this\" hx-swap=\"outerHTML\"\n  class=\"inline-flex w-full shrink-0 items-center justify-center gap-2 rounded-md px-4 py-2 text-sm font-medium whitespace-nowrap outline-none transition-all text-foreground hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 [&.htmx-request]:pointer-events-none [&.htmx-request]:opacity-70 {{ extra_class }}\"\n  {%- for k, v in attrs.items() %} {{ k|replace('_', '-') }}=\"{{ v }}\"{% endfor -%}\n><span class=\"htmx-indicator size-4 animate-spin rounded-full border-2 border-muted-foreground/30 border-t-muted-foreground\" aria-hidden=\"true\"></span>{% if caller %}{{ caller() }}{% else %}{{ label }}{% endif %}</button>\n{% endif %}\n{% endmacro %}\n"
    },
    {
      "path": "registry/go-templates/load-more.tmpl",
      "type": "registry:file",
      "target": "components/load-more.tmpl",
      "content": "{{/* Load More template — shadcn-htmx, htmx v4 + Tailwind v4.\n     Mirrors registry/ui/load-more.tsx.\n\n     A self-replacing pagination trigger. It appends the next page and swaps a\n     fresh trigger into its place (hx-swap=\"outerHTML\"); when the server omits\n     the trigger, the chain ends. Trigger \"click\" renders a real <button>\n     (works with no JS); \"intersect\"/\"revealed\" renders a scroll sentinel\n     (htmx infinite scroll, IntersectionObserver under the hood).\n\n     Sources cited in load-more.tsx:\n       repos/htmx/.../patterns/01-loading/01-click-to-load.md\n       repos/htmx/.../patterns/01-loading/02-infinite-scroll.md\n       repos/htmx/.../reference/01-attributes/{06-hx-trigger,07-hx-swap,19-hx-indicator}.md\n       repos/mdn/.../web/api/intersection_observer_api/index.md\n\n     Usage:\n       {{template \"load_more\" (dict \"Href\" \"/comments?page=2\" \"Label\" \"Show more comments\")}}\n       {{template \"load_more\" (dict \"Href\" \"/contacts?page=2\" \"Trigger\" \"intersect\")}} */}}\n\n{{define \"load_more\"}}\n{{- $trigger := or .Trigger \"click\" -}}\n{{- $label := or .Label \"Load more\" -}}\n{{- if or (eq $trigger \"intersect\") (eq $trigger \"revealed\") -}}\n<div {{if .ID}}id=\"{{.ID}}\"{{end}}\n     data-slot=\"load-more\" data-trigger=\"{{$trigger}}\"\n     role=\"status\" aria-label=\"{{or .AriaLabel \"Loading more\"}}\"\n     {{if .Href}}hx-get=\"{{.Href}}\"{{end}}\n     hx-trigger=\"{{if eq $trigger \"intersect\"}}intersect once{{else}}revealed{{end}}\" hx-swap=\"outerHTML\"\n     class=\"flex items-center justify-center gap-2 py-4 text-sm text-muted-foreground {{.Class}}\">{{if .Body}}{{.Body}}{{else}}<span class=\"htmx-indicator size-4 animate-spin rounded-full border-2 border-muted-foreground/30 border-t-muted-foreground\" aria-hidden=\"true\"></span>Loading more…{{end}}</div>\n{{- else -}}\n<button type=\"button\" {{if .ID}}id=\"{{.ID}}\"{{end}}\n        data-slot=\"load-more\" data-trigger=\"click\"\n        {{if .Disabled}}disabled{{end}}\n        {{if .AriaLabel}}aria-label=\"{{.AriaLabel}}\"{{end}}\n        {{if .Href}}hx-get=\"{{.Href}}\"{{end}}\n        hx-trigger=\"click\" hx-target=\"this\" hx-swap=\"outerHTML\"\n        class=\"inline-flex w-full shrink-0 items-center justify-center gap-2 rounded-md px-4 py-2 text-sm font-medium whitespace-nowrap outline-none transition-all text-foreground hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 [&.htmx-request]:pointer-events-none [&.htmx-request]:opacity-70 {{.Class}}\"><span class=\"htmx-indicator size-4 animate-spin rounded-full border-2 border-muted-foreground/30 border-t-muted-foreground\" aria-hidden=\"true\"></span>{{if .Body}}{{.Body}}{{else}}{{$label}}{{end}}</button>\n{{- end -}}\n{{end}}\n"
    },
    {
      "path": "registry/phoenix/load_more.ex",
      "type": "registry:file",
      "target": "lib/my_app_web/components/load_more.ex",
      "content": "defmodule ShadcnHtmx.Components.LoadMore do\n  @moduledoc \"\"\"\n  Load More — shadcn-htmx, htmx v4 + Tailwind v4 for Phoenix.\n\n  A self-replacing pagination trigger. It appends the next page and swaps a\n  fresh trigger into its place (`hx-swap=\"outerHTML\"`); when the server omits\n  the trigger from its response, the chain ends. Two modes:\n\n    * `trigger=\"click\"` — a real `<button>`. Works with no JS (it's a plain\n      button); htmx upgrades the click into a request.\n    * `trigger=\"intersect\"` / `trigger=\"revealed\"` — a scroll sentinel that\n      fires when it enters the viewport (IntersectionObserver under the hood).\n      Use `intersect` inside an `overflow-y: scroll` container, `revealed`\n      for the page viewport.\n\n  Sources (read, not copied) — see registry/ui/load-more.tsx:\n    repos/htmx/.../patterns/01-loading/01-click-to-load.md\n    repos/htmx/.../patterns/01-loading/02-infinite-scroll.md\n    repos/htmx/.../reference/01-attributes/{06-hx-trigger,07-hx-swap,19-hx-indicator}.md\n    repos/mdn/.../web/api/intersection_observer_api/index.md\n\n  ## Examples\n\n      <.load_more href={~p\"/comments?page=2\"} label=\"Show more comments\" />\n\n      <.load_more href={~p\"/contacts?page=2\"} trigger=\"intersect\" />\n  \"\"\"\n\n  use Phoenix.Component\n\n  attr :href, :string, default: nil\n  attr :trigger, :string, default: \"click\", values: ~w(click intersect revealed)\n  attr :label, :string, default: \"Load more\"\n  attr :\"aria-label\", :string, default: nil\n  attr :disabled, :boolean, default: false\n  attr :class, :string, default: nil\n  attr :rest, :global\n  slot :inner_block\n\n  def load_more(assigns) do\n    ~H\"\"\"\n    <%= if @trigger in [\"intersect\", \"revealed\"] do %>\n      <div\n        data-slot=\"load-more\"\n        data-trigger={@trigger}\n        role=\"status\"\n        aria-label={assigns[:\"aria-label\"] || \"Loading more\"}\n        hx-get={@href}\n        hx-trigger={if @trigger == \"intersect\", do: \"intersect once\", else: \"revealed\"}\n        hx-swap=\"outerHTML\"\n        class={[\"flex items-center justify-center gap-2 py-4 text-sm text-muted-foreground\", @class]}\n        {@rest}\n      >\n        <%= if @inner_block != [] do %>\n          {render_slot(@inner_block)}\n        <% else %>\n          <span\n            class=\"htmx-indicator size-4 animate-spin rounded-full border-2 border-muted-foreground/30 border-t-muted-foreground\"\n            aria-hidden=\"true\"\n          />\n          Loading more…\n        <% end %>\n      </div>\n    <% else %>\n      <button\n        type=\"button\"\n        data-slot=\"load-more\"\n        data-trigger=\"click\"\n        disabled={@disabled}\n        aria-label={assigns[:\"aria-label\"]}\n        hx-get={@href}\n        hx-trigger=\"click\"\n        hx-target=\"this\"\n        hx-swap=\"outerHTML\"\n        class={[\n          \"inline-flex w-full shrink-0 items-center justify-center gap-2 rounded-md px-4 py-2 text-sm font-medium whitespace-nowrap outline-none transition-all\",\n          \"text-foreground hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50\",\n          \"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50\",\n          \"disabled:pointer-events-none disabled:opacity-50\",\n          \"[&.htmx-request]:pointer-events-none [&.htmx-request]:opacity-70\",\n          @class\n        ]}\n        {@rest}\n      >\n        <span\n          class=\"htmx-indicator size-4 animate-spin rounded-full border-2 border-muted-foreground/30 border-t-muted-foreground\"\n          aria-hidden=\"true\"\n        />\n        <%= if @inner_block != [] do %>\n          {render_slot(@inner_block)}\n        <% else %>\n          {@label}\n        <% end %>\n      </button>\n    <% end %>\n    \"\"\"\n  end\nend\n"
    },
    {
      "path": "registry/html/load-more.html",
      "type": "registry:file",
      "target": "snippets/load-more.html",
      "content": "<!--\n  shadcn-htmx — raw HTML load-more snippet.\n  A self-replacing pagination trigger. It appends the next page and swaps a\n  fresh trigger into its place (hx-swap=\"outerHTML\" + hx-target=\"this\"); when\n  the server omits the trigger from its response, the chain ends.\n\n  Two modes shown below:\n    1. Click  — a real <button>. Works with no JS; htmx upgrades the click.\n    2. Sentinel — fires on scroll (hx-trigger=\"revealed\", or \"intersect once\"\n       inside an overflow-y:scroll container). IntersectionObserver-backed.\n\n  The inline spinner carries the htmx-indicator class, so it stays hidden\n  until htmx adds .htmx-request to the trigger during the in-flight request,\n  then fades in (the skeleton/loading fallback). Relies only on the theme\n  tokens in styles.css — no JS of its own.\n\n  Sources (read, not copied):\n    repos/htmx/.../patterns/01-loading/01-click-to-load.md\n    repos/htmx/.../patterns/01-loading/02-infinite-scroll.md\n    repos/htmx/.../reference/01-attributes/{06-hx-trigger,07-hx-swap,19-hx-indicator}.md\n    repos/mdn/.../web/api/intersection_observer_api/index.md\n-->\n\n<!-- 1. Click trigger. On click it GETs the next page and replaces itself with\n        (next items + a fresh trigger). Omit the trigger on the last page. -->\n<button type=\"button\" data-slot=\"load-more\" data-trigger=\"click\"\n        hx-get=\"/comments?page=2\" hx-trigger=\"click\" hx-target=\"this\" hx-swap=\"outerHTML\"\n        class=\"inline-flex w-full shrink-0 items-center justify-center gap-2 rounded-md px-4 py-2 text-sm font-medium whitespace-nowrap outline-none transition-all text-foreground hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 [&.htmx-request]:pointer-events-none [&.htmx-request]:opacity-70\">\n  <span class=\"htmx-indicator size-4 animate-spin rounded-full border-2 border-muted-foreground/30 border-t-muted-foreground\" aria-hidden=\"true\"></span>\n  Show more comments\n</button>\n\n<!-- 2. Scroll sentinel. When it scrolls into view, htmx GETs the next page\n        and replaces it. Use \"intersect once\" inside an overflow-y:scroll box. -->\n<div data-slot=\"load-more\" data-trigger=\"revealed\"\n     role=\"status\" aria-label=\"Loading more\"\n     hx-get=\"/contacts?page=2\" hx-trigger=\"revealed\" hx-swap=\"outerHTML\"\n     class=\"flex items-center justify-center gap-2 py-4 text-sm text-muted-foreground\">\n  <span class=\"htmx-indicator size-4 animate-spin rounded-full border-2 border-muted-foreground/30 border-t-muted-foreground\" aria-hidden=\"true\"></span>\n  Loading more…\n</div>\n"
    }
  ]
}
