{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "edit-in-place",
  "type": "registry:ui",
  "title": "Edit In Place",
  "description": "A read-only record view with an Edit affordance that swaps in a pre-filled form. Save issues a PUT and the server returns the updated view; Cancel re-fetches the view. The canonical htmx editable primitive — built entirely on outerHTML swaps over REST, no modal, no custom JS.",
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/ui/edit-in-place.tsx",
      "type": "registry:ui",
      "target": "components/ui/edit-in-place.tsx",
      "content": "/** @jsxImportSource hono/jsx */\nimport type { Child, PropsWithChildren } from \"hono/jsx\"\nimport { cn, type ClassValue } from \"@/registry/lib/cn\"\nimport { buttonClasses } from \"@/registry/ui/button\"\nimport { inputClasses } from \"@/registry/ui/input\"\nimport { labelClasses } from \"@/registry/ui/label\"\n\n// Edit In Place — shadcn-htmx, htmx v4 + Tailwind v4.\n//\n// The canonical htmx editable record: a read-only view with an \"Edit\"\n// affordance that swaps in a pre-filled form. Save = PUT; Cancel re-GETs\n// the view. No modal, no custom JS — the whole thing rides on outerHTML\n// swaps over REST.\n//\n// Built on:\n//   repos/htmx/www/src/content/patterns/03-records/04-edit-in-place.md\n//     (GET /users/1 → view, GET /users/1/edit → form, PUT /users/1 → view)\n//   repos/htmx/www/src/content/reference/01-attributes/08-hx-target.md:12-17\n//     hx-target=\"this\" targets the element making the request.\n//   repos/htmx/www/src/content/reference/01-attributes/07-hx-swap.md:35-41\n//     hx-swap=\"outerHTML\" replaces the whole element with the response.\n//   repos/htmx/www/src/content/reference/01-attributes/03-hx-put.md\n//     hx-put issues the REST PUT on Save.\n//\n// Native semantics:\n//   - The view is a <dl> description list — the right element for\n//     name/value record pairs.\n//     repos/mdn/files/en-us/web/html/reference/elements/dl/index.md\n//   - The edit affordance is a real <button> and the editor is a real\n//     <form> with native <input>s, so constraint validation, Enter-to-\n//     submit, and focus all come from the platform.\n//     repos/mdn/files/en-us/web/html/reference/elements/form/index.md\n//\n// Class strings mirror Card (rounded bordered container) + Input + Button,\n// so the view and the editor occupy the same visual footprint and the swap\n// looks like an in-place toggle rather than a layout jump.\n//\n// Style analogues: registry/ui/card.tsx, registry/ui/input.tsx,\n// registry/ui/button.tsx, registry/ui/label.tsx.\n\n// Shared container so the read-only view and the editor render at the same\n// size — the outerHTML swap then reads as a true in-place toggle.\nconst container =\n  \"flex w-full max-w-sm flex-col gap-4 rounded-xl border bg-card p-5 text-card-foreground shadow-sm\"\n\n// One field's label, styled as the small uppercase eyebrow shadcn uses for\n// record metadata.\nconst fieldTermClass =\n  \"text-xs font-medium tracking-wide text-muted-foreground uppercase\"\n\nconst fieldValueClass = \"mt-0.5 text-sm font-medium text-foreground\"\n\nexport type EditInPlaceField = {\n  // Visible label for the field, e.g. \"Email\".\n  label: string\n  // The current, read-only value shown in the view.\n  value: Child\n  // Form field name used by the editor's <input name>. Defaults to a\n  // lowercased label, but pass it explicitly for anything non-trivial.\n  name?: string\n  // Input type for the editor (text, email, url, tel, …). Default \"text\".\n  type?: string\n  // Raw value passed to the editor's <input value>. Falls back to `value`\n  // when that is a plain string.\n  inputValue?: string\n  required?: boolean\n}\n\n// ── View ────────────────────────────────────────────────────────────────\n// The read-only record. Carries hx-target=\"this\" + hx-swap=\"outerHTML\" so\n// any descendant request (the Edit button, and later the editor's Save /\n// Cancel) replaces this whole element. `editHref` is the GET that returns\n// the editor fragment.\n\ntype EditInPlaceProps = PropsWithChildren<{\n  // GET endpoint that returns the editor form fragment.\n  editHref: string\n  // Fields rendered as a <dl>. Omit when you pass custom children instead.\n  fields?: EditInPlaceField[]\n  // Label on the Edit button. Default \"Edit\".\n  editLabel?: string\n  id?: string\n  class?: ClassValue\n}> &\n  Record<string, any>\n\nexport function EditInPlace(props: EditInPlaceProps) {\n  const {\n    children,\n    editHref,\n    fields,\n    editLabel = \"Edit\",\n    class: className,\n    id,\n    ...rest\n  } = props\n\n  return (\n    <div\n      data-slot=\"edit-in-place\"\n      data-mode=\"view\"\n      id={id}\n      hx-target=\"this\"\n      hx-swap=\"outerHTML\"\n      class={cn(container, className)}\n      {...rest}\n    >\n      {fields ? (\n        <dl class=\"flex flex-col gap-3\" data-slot=\"edit-in-place-fields\">\n          {fields.map((f) => (\n            <div>\n              <dt class={fieldTermClass}>{f.label}</dt>\n              <dd class={fieldValueClass}>{f.value}</dd>\n            </div>\n          ))}\n        </dl>\n      ) : (\n        children\n      )}\n      <div class=\"flex\">\n        <button\n          type=\"button\"\n          data-slot=\"edit-in-place-edit\"\n          hx-get={editHref}\n          hx-target=\"closest [data-slot='edit-in-place']\"\n          hx-swap=\"outerHTML\"\n          class={buttonClasses({ variant: \"outline\", size: \"sm\" })}\n        >\n          {editLabel}\n        </button>\n      </div>\n    </div>\n  )\n}\n\n// ── Editor ──────────────────────────────────────────────────────────────\n// The pre-filled form returned by `editHref`. Submitting issues the PUT\n// (Save); Cancel re-GETs the view. Both target `this` form and swap\n// outerHTML, restoring the view in place.\n\ntype EditInPlaceFormProps = PropsWithChildren<{\n  // PUT endpoint hit on Save.\n  putHref: string\n  // GET endpoint that restores the read-only view on Cancel.\n  cancelHref: string\n  // Fields to pre-fill. Omit when you pass custom children instead.\n  fields?: EditInPlaceField[]\n  saveLabel?: string\n  cancelLabel?: string\n  id?: string\n  class?: ClassValue\n}> &\n  Record<string, any>\n\nexport function EditInPlaceForm(props: EditInPlaceFormProps) {\n  const {\n    children,\n    putHref,\n    cancelHref,\n    fields,\n    saveLabel = \"Save\",\n    cancelLabel = \"Cancel\",\n    class: className,\n    id,\n    ...rest\n  } = props\n\n  return (\n    <form\n      data-slot=\"edit-in-place\"\n      data-mode=\"edit\"\n      id={id}\n      hx-put={putHref}\n      hx-target=\"this\"\n      hx-swap=\"outerHTML\"\n      class={cn(container, className)}\n      {...rest}\n    >\n      {fields ? (\n        <div class=\"flex flex-col gap-3\" data-slot=\"edit-in-place-fields\">\n          {fields.map((f) => {\n            const name = f.name ?? f.label.toLowerCase()\n            const fieldId = `${id ?? \"field\"}-${name}`\n            const value =\n              f.inputValue ?? (typeof f.value === \"string\" ? f.value : undefined)\n            return (\n              <div class=\"grid gap-1.5\">\n                <label for={fieldId} class={labelClasses({ class: fieldTermClass })}>\n                  {f.label}\n                </label>\n                <input\n                  id={fieldId}\n                  name={name}\n                  type={f.type ?? \"text\"}\n                  value={value}\n                  required={f.required}\n                  data-slot=\"input\"\n                  class={inputClasses()}\n                />\n              </div>\n            )\n          })}\n        </div>\n      ) : (\n        children\n      )}\n      <div class=\"flex gap-2\">\n        <button\n          type=\"submit\"\n          data-slot=\"edit-in-place-save\"\n          class={buttonClasses({ size: \"sm\" })}\n        >\n          {saveLabel}\n        </button>\n        <button\n          type=\"button\"\n          data-slot=\"edit-in-place-cancel\"\n          hx-get={cancelHref}\n          hx-target=\"closest [data-slot='edit-in-place']\"\n          hx-swap=\"outerHTML\"\n          class={buttonClasses({ variant: \"secondary\", size: \"sm\" })}\n        >\n          {cancelLabel}\n        </button>\n      </div>\n    </form>\n  )\n}\n"
    },
    {
      "path": "registry/jinja2/edit-in-place.html",
      "type": "registry:file",
      "target": "templates/components/edit-in-place.html",
      "content": "{# Edit In Place macros — shadcn-htmx, htmx v4 + Tailwind v4.\n   Mirrors registry/ui/edit-in-place.tsx for Python/Flask/FastAPI/Django/Jinja2.\n\n   The canonical htmx editable record: a read-only view with an Edit button\n   that swaps in a pre-filled form. Save = PUT, Cancel re-GETs the view.\n   No modal, no custom JS — the whole thing rides on outerHTML swaps over REST.\n   See repos/htmx/www/src/content/patterns/03-records/04-edit-in-place.md.\n\n   Usage:\n       {% from \"components/edit-in-place.html\" import edit_in_place, edit_in_place_form %}\n\n       {# View — GET /users/1 returns this #}\n       {{ edit_in_place(edit_href=\"/users/1/edit\", id=\"user\", fields=[\n            {\"label\": \"Name\",  \"value\": user.name},\n            {\"label\": \"Email\", \"value\": user.email, \"type\": \"email\"},\n       ]) }}\n\n       {# Editor — GET /users/1/edit returns this #}\n       {{ edit_in_place_form(put_href=\"/users/1\", cancel_href=\"/users/1\", id=\"user\", fields=[\n            {\"label\": \"Name\",  \"value\": user.name},\n            {\"label\": \"Email\", \"value\": user.email, \"type\": \"email\"},\n       ]) }}\n\n   hx-target=\"this\" + hx-swap=\"outerHTML\" on each root mean every descendant\n   request replaces the whole element in place. #}\n\n{%- set _container = \"flex w-full max-w-sm flex-col gap-4 rounded-xl border bg-card p-5 text-card-foreground shadow-sm\" -%}\n{%- set _term = \"text-xs font-medium tracking-wide text-muted-foreground uppercase\" -%}\n{%- set _value = \"mt-0.5 text-sm font-medium text-foreground\" -%}\n{%- set _edit_btn = \"inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&.htmx-request]:pointer-events-none [&.htmx-request]:opacity-70 border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50 h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5\" -%}\n{%- set _save_btn = \"inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&.htmx-request]:pointer-events-none [&.htmx-request]:opacity-70 bg-primary text-primary-foreground hover:bg-primary/90 h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5\" -%}\n{%- set _cancel_btn = \"inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&.htmx-request]:pointer-events-none [&.htmx-request]:opacity-70 bg-secondary text-secondary-foreground hover:bg-secondary/80 h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5\" -%}\n{%- set _input = \"flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&.htmx-request]:opacity-70\" -%}\n{%- set _label = \"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50 text-xs font-medium tracking-wide text-muted-foreground uppercase\" -%}\n\n{# View — read-only record + Edit button. #}\n{% macro edit_in_place(edit_href, fields=none, edit_label=\"Edit\", id=none, extra_class=\"\", caller=none) %}\n<div data-slot=\"edit-in-place\" data-mode=\"view\"\n     {%- if id %} id=\"{{ id }}\"{% endif %}\n     hx-target=\"this\" hx-swap=\"outerHTML\"\n     class=\"{{ _container }} {{ extra_class }}\">\n  {%- if fields %}\n  <dl class=\"flex flex-col gap-3\" data-slot=\"edit-in-place-fields\">\n    {%- for f in fields %}\n    <div>\n      <dt class=\"{{ _term }}\">{{ f.label }}</dt>\n      <dd class=\"{{ _value }}\">{{ f.value }}</dd>\n    </div>\n    {%- endfor %}\n  </dl>\n  {%- elif caller %}{{ caller() }}{% endif %}\n  <div class=\"flex\">\n    <button type=\"button\" data-slot=\"edit-in-place-edit\" hx-get=\"{{ edit_href }}\" hx-target=\"closest [data-slot='edit-in-place']\" hx-swap=\"outerHTML\" class=\"{{ _edit_btn }}\">{{ edit_label }}</button>\n  </div>\n</div>\n{% endmacro %}\n\n{# Editor — pre-filled form. Save submits the PUT; Cancel re-GETs the view. #}\n{% macro edit_in_place_form(put_href, cancel_href, fields=none, save_label=\"Save\", cancel_label=\"Cancel\", id=none, extra_class=\"\", caller=none) %}\n<form data-slot=\"edit-in-place\" data-mode=\"edit\"\n      {%- if id %} id=\"{{ id }}\"{% endif %}\n      hx-put=\"{{ put_href }}\" hx-target=\"this\" hx-swap=\"outerHTML\"\n      class=\"{{ _container }} {{ extra_class }}\">\n  {%- if fields %}\n  <div class=\"flex flex-col gap-3\" data-slot=\"edit-in-place-fields\">\n    {%- for f in fields %}\n    {%- set name = f.name if f.name else f.label|lower %}\n    {%- set field_id = (id if id else \"field\") ~ \"-\" ~ name %}\n    <div class=\"grid gap-1.5\">\n      <label for=\"{{ field_id }}\" class=\"{{ _label }}\">{{ f.label }}</label>\n      <input id=\"{{ field_id }}\" name=\"{{ name }}\" type=\"{{ f.type if f.type else 'text' }}\"\n             {%- if f.value is not none %} value=\"{{ f.value }}\"{% endif %}\n             {%- if f.required %} required{% endif %}\n             data-slot=\"input\" class=\"{{ _input }}\">\n    </div>\n    {%- endfor %}\n  </div>\n  {%- elif caller %}{{ caller() }}{% endif %}\n  <div class=\"flex gap-2\">\n    <button type=\"submit\" data-slot=\"edit-in-place-save\" class=\"{{ _save_btn }}\">{{ save_label }}</button>\n    <button type=\"button\" data-slot=\"edit-in-place-cancel\" hx-get=\"{{ cancel_href }}\" hx-target=\"closest [data-slot='edit-in-place']\" hx-swap=\"outerHTML\" class=\"{{ _cancel_btn }}\">{{ cancel_label }}</button>\n  </div>\n</form>\n{% endmacro %}\n"
    },
    {
      "path": "registry/go-templates/edit-in-place.tmpl",
      "type": "registry:file",
      "target": "components/edit-in-place.tmpl",
      "content": "{{/*\n  Edit In Place templates — shadcn-htmx, htmx v4 + Tailwind v4.\n  Mirrors registry/ui/edit-in-place.tsx for Go projects using html/template.\n\n  The canonical htmx editable record: a read-only view with an Edit button\n  that swaps in a pre-filled form. Save = PUT, Cancel re-GETs the view. No\n  modal, no custom JS — the whole thing rides on outerHTML swaps over REST.\n  See repos/htmx/www/src/content/patterns/03-records/04-edit-in-place.md.\n\n  Two templates:\n    - \"edit_in_place\"      — the read-only view (GET /users/1 returns this).\n    - \"edit_in_place_form\" — the pre-filled editor (GET /users/1/edit).\n\n  Field struct shared by both:\n\n      type EipField struct {\n          Label    string // \"Email\"\n          Value    string // current value (shown in view, prefilled in editor)\n          Name     string // form field name (required in the editor)\n          Type     string // input type (default \"text\")\n          Required bool\n      }\n\n  View args:\n\n      type EipArgs struct {\n          ID        string\n          EditHref  string       // GET endpoint returning the editor\n          EditLabel string       // default \"Edit\"\n          Fields    []EipField\n      }\n      tpl.ExecuteTemplate(w, \"edit_in_place\", EipArgs{\n          ID: \"user\", EditHref: \"/users/1/edit\",\n          Fields: []EipField{\n              {Label: \"Name\", Value: \"Joe Smith\"},\n              {Label: \"Email\", Value: \"joe@smith.org\", Type: \"email\"},\n          },\n      })\n\n  Editor args:\n\n      type EipFormArgs struct {\n          ID          string\n          PutHref     string     // PUT endpoint hit on Save\n          CancelHref  string     // GET endpoint restoring the view on Cancel\n          SaveLabel   string     // default \"Save\"\n          CancelLabel string     // default \"Cancel\"\n          Fields      []EipField\n      }\n\n  hx-target=\"this\" + hx-swap=\"outerHTML\" on each root mean every descendant\n  request replaces the whole element in place.\n*/}}\n\n{{define \"edit_in_place\"}}\n{{- $container := \"flex w-full max-w-sm flex-col gap-4 rounded-xl border bg-card p-5 text-card-foreground shadow-sm\" -}}\n{{- $term := \"text-xs font-medium tracking-wide text-muted-foreground uppercase\" -}}\n{{- $value := \"mt-0.5 text-sm font-medium text-foreground\" -}}\n{{- $editBtn := \"inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&.htmx-request]:pointer-events-none [&.htmx-request]:opacity-70 border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50 h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5\" -}}\n{{- $editLabel := or .EditLabel \"Edit\" -}}\n<div data-slot=\"edit-in-place\" data-mode=\"view\"\n     {{- if .ID}} id=\"{{.ID}}\"{{end}}\n     hx-target=\"this\" hx-swap=\"outerHTML\"\n     class=\"{{$container}}\">\n  <dl class=\"flex flex-col gap-3\" data-slot=\"edit-in-place-fields\">\n    {{- range .Fields}}\n    <div>\n      <dt class=\"{{$term}}\">{{.Label}}</dt>\n      <dd class=\"{{$value}}\">{{.Value}}</dd>\n    </div>\n    {{- end}}\n  </dl>\n  <div class=\"flex\">\n    <button type=\"button\" data-slot=\"edit-in-place-edit\" hx-get=\"{{.EditHref}}\" hx-target=\"closest [data-slot='edit-in-place']\" hx-swap=\"outerHTML\" class=\"{{$editBtn}}\">{{$editLabel}}</button>\n  </div>\n</div>\n{{end}}\n\n{{define \"edit_in_place_form\"}}\n{{- $container := \"flex w-full max-w-sm flex-col gap-4 rounded-xl border bg-card p-5 text-card-foreground shadow-sm\" -}}\n{{- $label := \"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50 text-xs font-medium tracking-wide text-muted-foreground uppercase\" -}}\n{{- $input := \"flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&.htmx-request]:opacity-70\" -}}\n{{- $saveBtn := \"inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&.htmx-request]:pointer-events-none [&.htmx-request]:opacity-70 bg-primary text-primary-foreground hover:bg-primary/90 h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5\" -}}\n{{- $cancelBtn := \"inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&.htmx-request]:pointer-events-none [&.htmx-request]:opacity-70 bg-secondary text-secondary-foreground hover:bg-secondary/80 h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5\" -}}\n{{- $saveLabel := or .SaveLabel \"Save\" -}}\n{{- $cancelLabel := or .CancelLabel \"Cancel\" -}}\n{{- $id := or .ID \"field\" -}}\n<form data-slot=\"edit-in-place\" data-mode=\"edit\"\n      {{- if .ID}} id=\"{{.ID}}\"{{end}}\n      hx-put=\"{{.PutHref}}\" hx-target=\"this\" hx-swap=\"outerHTML\"\n      class=\"{{$container}}\">\n  <div class=\"flex flex-col gap-3\" data-slot=\"edit-in-place-fields\">\n    {{- range .Fields}}\n    {{- $name := .Name}}\n    {{- $fieldId := printf \"%s-%s\" $id $name}}\n    <div class=\"grid gap-1.5\">\n      <label for=\"{{$fieldId}}\" class=\"{{$label}}\">{{.Label}}</label>\n      <input id=\"{{$fieldId}}\" name=\"{{$name}}\" type=\"{{or .Type \"text\"}}\"\n             value=\"{{.Value}}\"\n             {{- if .Required}} required{{end}}\n             data-slot=\"input\" class=\"{{$input}}\">\n    </div>\n    {{- end}}\n  </div>\n  <div class=\"flex gap-2\">\n    <button type=\"submit\" data-slot=\"edit-in-place-save\" class=\"{{$saveBtn}}\">{{$saveLabel}}</button>\n    <button type=\"button\" data-slot=\"edit-in-place-cancel\" hx-get=\"{{.CancelHref}}\" hx-target=\"closest [data-slot='edit-in-place']\" hx-swap=\"outerHTML\" class=\"{{$cancelBtn}}\">{{$cancelLabel}}</button>\n  </div>\n</form>\n{{end}}\n"
    },
    {
      "path": "registry/phoenix/edit_in_place.ex",
      "type": "registry:file",
      "target": "lib/my_app_web/components/edit_in_place.ex",
      "content": "defmodule ShadcnHtmx.Components.EditInPlace do\n  @moduledoc \"\"\"\n  Edit In Place — shadcn-htmx, htmx v4 + Tailwind v4 for Phoenix.\n\n  Mirrors registry/ui/edit-in-place.tsx. The canonical htmx editable record:\n  a read-only view with an Edit button that swaps in a pre-filled form.\n  Save = PUT, Cancel re-GETs the view. No modal, no custom JS — the whole\n  thing rides on outerHTML swaps over REST.\n  See repos/htmx/www/src/content/patterns/03-records/04-edit-in-place.md.\n\n  Two function components:\n    - `edit_in_place/1`      — the read-only view (GET /users/1 returns this).\n    - `edit_in_place_form/1` — the pre-filled editor (GET /users/1/edit).\n\n  Each `:fields` entry is a map:\n\n      %{label: \"Email\", value: \"joe@smith.org\", name: \"email\", type: \"email\", required: true}\n\n  ## Examples\n\n      <%# View %>\n      <.edit_in_place id=\"user\" edit_href=\"/users/1/edit\" fields={[\n        %{label: \"Name\", value: @user.name},\n        %{label: \"Email\", value: @user.email, type: \"email\"}\n      ]} />\n\n      <%# Editor %>\n      <.edit_in_place_form id=\"user\" put_href=\"/users/1\" cancel_href=\"/users/1\" fields={[\n        %{label: \"Name\", value: @user.name},\n        %{label: \"Email\", value: @user.email, type: \"email\"}\n      ]} />\n\n  hx-target=\"this\" + hx-swap=\"outerHTML\" on each root mean every descendant\n  request replaces the whole element in place.\n  \"\"\"\n\n  use Phoenix.Component\n\n  @container \"flex w-full max-w-sm flex-col gap-4 rounded-xl border bg-card p-5 text-card-foreground shadow-sm\"\n  @term \"text-xs font-medium tracking-wide text-muted-foreground uppercase\"\n  @value \"mt-0.5 text-sm font-medium text-foreground\"\n  @label \"flex items-center gap-2 text-sm leading-none font-medium select-none \" <>\n            \"group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 \" <>\n            \"peer-disabled:cursor-not-allowed peer-disabled:opacity-50 \" <>\n            \"text-xs font-medium tracking-wide text-muted-foreground uppercase\"\n  @input \"flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs \" <>\n            \"transition-[color,box-shadow] outline-none \" <>\n            \"selection:bg-primary selection:text-primary-foreground \" <>\n            \"file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground \" <>\n            \"placeholder:text-muted-foreground \" <>\n            \"disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 \" <>\n            \"md:text-sm dark:bg-input/30 \" <>\n            \"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 \" <>\n            \"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 \" <>\n            \"[&.htmx-request]:opacity-70\"\n  @btn_base \"inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none \" <>\n              \"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 \" <>\n              \"disabled:pointer-events-none disabled:opacity-50 \" <>\n              \"aria-disabled:pointer-events-none aria-disabled:opacity-50 \" <>\n              \"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 \" <>\n              \"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 \" <>\n              \"[&.htmx-request]:pointer-events-none [&.htmx-request]:opacity-70 \" <>\n              \"h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5\"\n  @edit_btn @btn_base <>\n              \" border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50\"\n  @save_btn @btn_base <> \" bg-primary text-primary-foreground hover:bg-primary/90\"\n  @cancel_btn @btn_base <> \" bg-secondary text-secondary-foreground hover:bg-secondary/80\"\n\n  attr :id, :string, default: nil\n  attr :edit_href, :string, required: true\n  attr :edit_label, :string, default: \"Edit\"\n  attr :fields, :list, default: []\n  attr :class, :string, default: nil\n  attr :rest, :global, include: ~w(hx-get hx-post hx-put hx-patch hx-target hx-swap hx-trigger hx-indicator)\n\n  def edit_in_place(assigns) do\n    assigns =\n      assigns\n      |> assign(:container, @container)\n      |> assign(:term, @term)\n      |> assign(:value, @value)\n      |> assign(:edit_btn, @edit_btn)\n\n    ~H\"\"\"\n    <div\n      data-slot=\"edit-in-place\"\n      data-mode=\"view\"\n      id={@id}\n      hx-target=\"this\"\n      hx-swap=\"outerHTML\"\n      class={[@container, @class]}\n      {@rest}\n    >\n      <dl class=\"flex flex-col gap-3\" data-slot=\"edit-in-place-fields\">\n        <div :for={f <- @fields}>\n          <dt class={@term}>{f.label}</dt>\n          <dd class={@value}>{f.value}</dd>\n        </div>\n      </dl>\n      <div class=\"flex\">\n        <button\n          type=\"button\"\n          data-slot=\"edit-in-place-edit\"\n          hx-get={@edit_href}\n          hx-target=\"closest [data-slot='edit-in-place']\"\n          hx-swap=\"outerHTML\"\n          class={@edit_btn}\n        >\n          {@edit_label}\n        </button>\n      </div>\n    </div>\n    \"\"\"\n  end\n\n  attr :id, :string, default: nil\n  attr :put_href, :string, required: true\n  attr :cancel_href, :string, required: true\n  attr :save_label, :string, default: \"Save\"\n  attr :cancel_label, :string, default: \"Cancel\"\n  attr :fields, :list, default: []\n  attr :class, :string, default: nil\n  attr :rest, :global, include: ~w(hx-target hx-swap hx-trigger hx-indicator)\n\n  def edit_in_place_form(assigns) do\n    base_id = assigns.id || \"field\"\n\n    assigns =\n      assigns\n      |> assign(:base_id, base_id)\n      |> assign(:container, @container)\n      |> assign(:label, @label)\n      |> assign(:input, @input)\n      |> assign(:save_btn, @save_btn)\n      |> assign(:cancel_btn, @cancel_btn)\n\n    ~H\"\"\"\n    <form\n      data-slot=\"edit-in-place\"\n      data-mode=\"edit\"\n      id={@id}\n      hx-put={@put_href}\n      hx-target=\"this\"\n      hx-swap=\"outerHTML\"\n      class={[@container, @class]}\n      {@rest}\n    >\n      <div class=\"flex flex-col gap-3\" data-slot=\"edit-in-place-fields\">\n        <div :for={f <- @fields} class=\"grid gap-1.5\">\n          <label for={\"#{@base_id}-#{f[:name] || String.downcase(f.label)}\"} class={@label}>\n            {f.label}\n          </label>\n          <input\n            id={\"#{@base_id}-#{f[:name] || String.downcase(f.label)}\"}\n            name={f[:name] || String.downcase(f.label)}\n            type={f[:type] || \"text\"}\n            value={f.value}\n            required={f[:required] || nil}\n            data-slot=\"input\"\n            class={@input}\n          />\n        </div>\n      </div>\n      <div class=\"flex gap-2\">\n        <button type=\"submit\" data-slot=\"edit-in-place-save\" class={@save_btn}>\n          {@save_label}\n        </button>\n        <button\n          type=\"button\"\n          data-slot=\"edit-in-place-cancel\"\n          hx-get={@cancel_href}\n          hx-target=\"closest [data-slot='edit-in-place']\"\n          hx-swap=\"outerHTML\"\n          class={@cancel_btn}\n        >\n          {@cancel_label}\n        </button>\n      </div>\n    </form>\n    \"\"\"\n  end\nend\n"
    },
    {
      "path": "registry/html/edit-in-place.html",
      "type": "registry:file",
      "target": "snippets/edit-in-place.html",
      "content": "<!--\n  shadcn-htmx — raw HTML Edit In Place snippet.\n\n  Mirrors registry/ui/edit-in-place.tsx. Drop onto any page that loads\n  htmx v4 + Tailwind CSS v4 and the shadcn theme variables (card, border,\n  ring, primary, secondary, muted, foreground). Relies only on theme tokens.\n\n  The canonical htmx editable record: a read-only VIEW with an Edit button\n  that GETs the EDITOR form; Save issues the PUT; Cancel re-GETs the view.\n  No modal, no custom JS — the whole thing rides on outerHTML swaps over REST.\n  See repos/htmx/www/src/content/patterns/03-records/04-edit-in-place.md.\n\n  REST endpoints (the URL is the resource, the method is the action):\n    GET  /users/1        → the view fragment below\n    GET  /users/1/edit   → the editor fragment below\n    PUT  /users/1        → updates, then returns the view fragment\n\n  hx-target=\"this\" + hx-swap=\"outerHTML\" on each root mean every descendant\n  request (Edit / Save / Cancel) replaces the whole element in place.\n-->\n\n<!-- ─── VIEW (GET /users/1) ─────────────────────────────────────────── -->\n<div data-slot=\"edit-in-place\" data-mode=\"view\" id=\"user\"\n     hx-target=\"this\" hx-swap=\"outerHTML\"\n     class=\"flex w-full max-w-sm flex-col gap-4 rounded-xl border bg-card p-5 text-card-foreground shadow-sm\">\n  <dl class=\"flex flex-col gap-3\" data-slot=\"edit-in-place-fields\">\n    <div>\n      <dt class=\"text-xs font-medium tracking-wide text-muted-foreground uppercase\">Name</dt>\n      <dd class=\"mt-0.5 text-sm font-medium text-foreground\">Joe Smith</dd>\n    </div>\n    <div>\n      <dt class=\"text-xs font-medium tracking-wide text-muted-foreground uppercase\">Email</dt>\n      <dd class=\"mt-0.5 text-sm font-medium text-foreground\">joe@smith.org</dd>\n    </div>\n  </dl>\n  <div class=\"flex\">\n    <button type=\"button\" data-slot=\"edit-in-place-edit\" hx-get=\"/users/1/edit\"\n            hx-target=\"closest [data-slot='edit-in-place']\" hx-swap=\"outerHTML\"\n            class=\"inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&.htmx-request]:pointer-events-none [&.htmx-request]:opacity-70 border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50 h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5\">Edit</button>\n  </div>\n</div>\n\n<!-- ─── EDITOR (GET /users/1/edit) ──────────────────────────────────── -->\n<form data-slot=\"edit-in-place\" data-mode=\"edit\" id=\"user\"\n      hx-put=\"/users/1\" hx-target=\"this\" hx-swap=\"outerHTML\"\n      class=\"flex w-full max-w-sm flex-col gap-4 rounded-xl border bg-card p-5 text-card-foreground shadow-sm\">\n  <div class=\"flex flex-col gap-3\" data-slot=\"edit-in-place-fields\">\n    <div class=\"grid gap-1.5\">\n      <label for=\"user-name\" class=\"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50 text-xs font-medium tracking-wide text-muted-foreground uppercase\">Name</label>\n      <input id=\"user-name\" name=\"name\" type=\"text\" value=\"Joe Smith\" data-slot=\"input\"\n             class=\"flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&.htmx-request]:opacity-70\">\n    </div>\n    <div class=\"grid gap-1.5\">\n      <label for=\"user-email\" class=\"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50 text-xs font-medium tracking-wide text-muted-foreground uppercase\">Email</label>\n      <input id=\"user-email\" name=\"email\" type=\"email\" value=\"joe@smith.org\" data-slot=\"input\"\n             class=\"flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30 focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&.htmx-request]:opacity-70\">\n    </div>\n  </div>\n  <div class=\"flex gap-2\">\n    <button type=\"submit\" data-slot=\"edit-in-place-save\"\n            class=\"inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&.htmx-request]:pointer-events-none [&.htmx-request]:opacity-70 bg-primary text-primary-foreground hover:bg-primary/90 h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5\">Save</button>\n    <button type=\"button\" data-slot=\"edit-in-place-cancel\" hx-get=\"/users/1\"\n            hx-target=\"closest [data-slot='edit-in-place']\" hx-swap=\"outerHTML\"\n            class=\"inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&.htmx-request]:pointer-events-none [&.htmx-request]:opacity-70 bg-secondary text-secondary-foreground hover:bg-secondary/80 h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5\">Cancel</button>\n  </div>\n</form>\n"
    }
  ]
}
