diff --git a/front/Manifest.toml b/front/Manifest.toml
index 3de4c3baa3ba5bb3930c2adde55d3ba205fee0cf..bb7f14771020bcaa002752f4435a92a481dd129a 100644
--- a/front/Manifest.toml
+++ b/front/Manifest.toml
@@ -427,7 +427,7 @@ version = "1.0.0"
 
 [[deps.NyxPlots]]
 deps = ["Bonito", "Observables", "PlotlyBase"]
-git-tree-sha1 = "86ee6e2a8996e9bff6bfce8c41a71afcedd420a0"
+git-tree-sha1 = "e54fcb78c5337200724a3f1b6c6401e645a5263f"
 repo-rev = "main"
 repo-url = "https://gitlab.com/dbc-nyx/NyxPlots.jl"
 uuid = "e8b8ccdb-0776-4145-b74f-57bbbfff4409"
diff --git a/src/apps/muscles/MuscleWidgets.jl b/src/apps/muscles/MuscleWidgets.jl
index 398c251c2979d40f5c6022bb87357b43a6b840b5..786a5c7cabae2aa0dc61af73aa9ef48cf2f36a9f 100644
--- a/src/apps/muscles/MuscleWidgets.jl
+++ b/src/apps/muscles/MuscleWidgets.jl
@@ -1,5 +1,6 @@
 module MuscleWidgets
 
+using Base: has_nondefault_cmd_flags
 using NyxWidgets.Players, NyxWidgets.Muscles, NyxWidgets.AnimatedLayers
 import NyxWidgets.Base: lowerdom, dom_id, dom_id!, Div
 using NyxPlots
@@ -49,7 +50,7 @@ function MuscleWidget(sequence)
                                                segmentlabel=true,
                                                style="width: 40%; margin-top: 2rem;")
                           for segment in segments(overview)]
-    moveto = nothing
+    moveto = clipboard = nothing
     heatmaps = Heatmap[]
     for segment in individualsegments
         halfsegment = sequence[][segment.segment, segment.side]
@@ -59,8 +60,11 @@ function MuscleWidget(sequence)
                           colorscale=colormap,
                           width=606, height=800,
                           zmin=0., zmax=1.,
-                          click=moveto)
-        moveto = heatmap.click
+                          pick=moveto,
+                          clipboard=clipboard,
+                          setvalue=setvalue)
+        moveto = heatmap.pick
+        clipboard = heatmap.clipboard
         push!(heatmaps, heatmap)
     end
     detailedviews = [Div(widget, heatmap; style=:row)
@@ -111,15 +115,10 @@ function MuscleWidget(sequence)
         @info "Click on muscle" name=xyz[2] step value=xyz[3]
     end
     for (segment, heatmap) in zip(individualsegments, heatmaps)
-        on(heatmap.select) do coord
-            x, y = coord
-            @assert !isempty(y)
+        onany(heatmap.assign, heatmap.paste) do _, _
             seq = sequence[][segment.segment, segment.side]
-            t0, t1 = [findfirst(==(convert(Float64, t)), seq.times) for t in x]
-            foreach(y) do muscle
-                seq[muscle, t0:t1] = setvalue[]
-            end
-            Heatmaps.update(heatmap, convert(Matrix, seq))
+            @debug "heatmap" heatmap.z[]
+            seq.activity = heatmap.z[]
         end
     end
     on(colormap) do colorscale
@@ -129,13 +128,13 @@ function MuscleWidget(sequence)
     on(animation.switchlayer) do layers
         current, _, _ = layers
         if 1 < current
-            heatmaps[current-1].selecting[] = editmode[]
+            heatmaps[current-1].editing[] = editmode[]
         end
     end
     on(editmode) do b
         layer = activelayer(Int, animation)
         if 1 < layer
-            heatmaps[layer-1].selecting[] = b
+            heatmaps[layer-1].editing[] = b
         end
     end
     return MuscleWidget(sequence, colormap, colorscheme, setvalue, overview,
@@ -225,4 +224,15 @@ function clear(widget)
     end
 end
 
+clipboard(widget::MuscleWidget) = clipboard(widget.heatmaps[1])
+
+clipboard(heatmap::Heatmap) = heatmap.clipboard
+
+function areaselect(widget::MuscleWidget)
+    obs = Observable(true)
+    f(values...) = notify(obs)
+    onany(f, [h.select for h in widget.heatmaps]...)
+    return obs
+end
+
 end
diff --git a/src/apps/muscles/app.jl b/src/apps/muscles/app.jl
index 619b12b911ae9b4b8d7e7b64850f926054e35ff4..b24ccc970093d408c5bd7e24ecb17515264c34e1 100644
--- a/src/apps/muscles/app.jl
+++ b/src/apps/muscles/app.jl
@@ -1,6 +1,7 @@
 module MuscleApp
 
 using NyxUI, NyxUI.Storage
+using Observables
 using Genie, GenieSession, Stipple, StippleUI
 import Stipple: @app, @init, @private, @in, @out, @onchange, @onbutton, @notify
 
@@ -9,9 +10,11 @@ using .Backbone
 
 export muscle_view
 
-const default_colormap = Backbone.MuscleWidgets.__default_colormap__
+const MuscleWidgets = Backbone.MuscleWidgets
 
-const colormaps = Backbone.MuscleWidgets.__colormaps__
+const default_colormap = MuscleWidgets.__default_colormap__
+
+const colormaps = MuscleWidgets.__colormaps__
 
 const maxlength = getconfig("muscle-activity", "maxlength")
 
@@ -41,6 +44,9 @@ const bonito_app = NamedApp(:inherit, Backbone.app)
     @in time_interval = 0.05
     @in start_time = 0.0
 
+    @out area_selected = false
+    @out new_clipboard_item = false
+
     @onchange isready begin
         _, url = init_model(__model__)
         run(__model__, """
@@ -51,12 +57,12 @@ const bonito_app = NamedApp(:inherit, Backbone.app)
         iframe.src = '$url';
         """)
         session_id = back_session_id
+        model = getmodel(session_id)
         if true # TODO: diagnose missing Stipple initialization
             # restore state after page reload (Ctrl+R)
             notify(sequence_names)
             selected_sequence_name = name = getsequence(session_id).program_name
             selected_sequence_index = @something findfirst(==(name), sequence_names[!]) 0
-            model = getmodel(session_id)
             colormap = model.colormap[]
             editmode = model.editmode[]
             sequence_name = name
@@ -66,6 +72,14 @@ const bonito_app = NamedApp(:inherit, Backbone.app)
             time_interval = step(seq.times)
             start_time = first(seq.times)
         end
+        on(MuscleWidgets.clipboard(model)) do _
+            new_clipboard_item[!] = false
+            new_clipboard_item = true
+        end
+        on(MuscleWidgets.areaselect(model)) do _
+            area_selected[!] = false
+            area_selected = true
+        end
     end
 
     @onchange start_time, time_interval, series_length begin
@@ -77,21 +91,21 @@ const bonito_app = NamedApp(:inherit, Backbone.app)
             if time_interval < 0
                 # do not trigger on 0, because editing the corresponding field often implies
                 # typing "0." first
-                @notify "time interval must be strictly positive" :info
+                @notify "Time interval must be strictly positive" :error
                 time_interval = step(seq.times)
             end
             valid = false
         end
         if series_length < 1
-            @notify "time series length must be strictly positive" :info
+            @notify "Time series length must be strictly positive" :error
             series_length = length(seq.times)
             valid = false
         elseif round(series_length) != series_length
-            @notify "time series length must be integer" :info
+            @notify "Time series length must be an integer" :error
             series_length = round(series_length)
             valid = false
         elseif !isnothing(maxlength) && maxlength < series_length
-            @notify "time series is too long" :info
+            @notify "Time series is too long" :error
             series_length = length(seq.times)
             valid = false
         end
@@ -176,7 +190,7 @@ const bonito_app = NamedApp(:inherit, Backbone.app)
             sequence_names[!][selected_sequence_index] = sequence_name
             notify(sequence_names)
         else
-            @notify "Motor program name contains prohibited characters" :info
+            @notify "Motor program name contains prohibited characters" :error
             sequence_name = sequence[!].program_name
         end
     end
@@ -200,7 +214,7 @@ const bonito_app = NamedApp(:inherit, Backbone.app)
     @onbutton export_sequence_click begin
         filepath = exportsequence(back_session_id)
         if isempty(filepath)
-            @notify "No available motor programs; create one first" :info
+            @notify "No available motor programs; create one first" :error
         else
             filename = basename(filepath)
             mkpath("./public/$back_session_id")
@@ -242,6 +256,13 @@ const bonito_app = NamedApp(:inherit, Backbone.app)
             fileuploads = empty!(fileuploads)
         end
     end
+
+    @onchange new_clipboard_item begin
+        @notify  "Selection copied! Press Ctrl while clicking to paste" :info
+    end
+    @onchange area_selected begin
+        @notify "Click inside the selected area to assign the setvalue, or press Ctrl while clicking to copy the selection" :info
+    end
 end
 
 function dropdown(key, options, label; margin=true, class=nothing)