@@ -55,7 +55,9 @@ function readElementPosition(
5555 const element = sel . element ;
5656 if ( ! element ?. isConnected || ! gsap ?. getProperty ) return result ;
5757
58- const props = anim ? Object . keys ( anim . properties ) : [ "x" , "y" , "opacity" ] ;
58+ // ponytail: a brand-new tween captures position only — bundling opacity made it
59+ // a mixed group that the position-only drag intercept couldn't resolve.
60+ const props = anim ? Object . keys ( anim . properties ) : [ "x" , "y" ] ;
5961 for ( const prop of props ) {
6062 const val = Number ( gsap . getProperty ( element , prop ) ) ;
6163 if ( ! Number . isFinite ( val ) ) continue ;
@@ -65,6 +67,32 @@ function readElementPosition(
6567 return result ;
6668}
6769
70+ /**
71+ * Range for a brand-new keyframe tween created via "Enable keyframes" on an element
72+ * with no existing animation. "Add a keyframe" must land at the PLAYHEAD.
73+ *
74+ * The runtime auto-stamps `data-start="0"` + `data-duration=<rootDuration>` on every
75+ * timeline element, so we can't treat `data-start` as authored timing (doing so put
76+ * the keyframe at 0). Instead, clamp the playhead into the element's [start, end]
77+ * range: the auto-stamp's full-composition range passes the playhead through
78+ * unchanged, while a genuinely narrow authored clip still clamps sensibly.
79+ */
80+ export function resolveNewTweenRange (
81+ authoredStart : string | undefined ,
82+ authoredDuration : string | undefined ,
83+ currentTime : number ,
84+ ) : { start : number ; duration : number } {
85+ const t = Math . max ( 0 , roundTo3 ( currentTime ) ) ;
86+ const start = authoredStart != null ? Number . parseFloat ( authoredStart ) : Number . NaN ;
87+ const duration = authoredDuration != null ? Number . parseFloat ( authoredDuration ) : Number . NaN ;
88+ if ( ! Number . isFinite ( start ) || ! Number . isFinite ( duration ) || duration <= 0 ) {
89+ return { start : t , duration : 1 } ;
90+ }
91+ const end = start + duration ;
92+ const clampedStart = Math . min ( Math . max ( t , start ) , end ) ;
93+ return { start : clampedStart , duration : Math . max ( 0.5 , roundTo3 ( end - clampedStart ) ) } ;
94+ }
95+
6896async function fetchAnimationsForElement ( sel : DomEditSelection ) : Promise < GsapAnimation [ ] > {
6997 const projectId = window . location . hash . match ( / p r o j e c t \/ ( [ ^ ? / ] + ) / ) ?. [ 1 ] ;
7098 if ( ! projectId ) return [ ] ;
@@ -122,9 +150,11 @@ export function useEnableKeyframes(
122150 }
123151 } else {
124152 const position = readElementPosition ( iframe , sel , null ) ;
125- const pct = computeElementPercentage ( t , sel ) ;
126- const elStart = Number . parseFloat ( sel . dataAttributes ?. start ?? "0" ) || 0 ;
127- const elDuration = Number . parseFloat ( sel . dataAttributes ?. duration ?? "1" ) || 1 ;
153+ const { start : elStart , duration : elDuration } = resolveNewTweenRange (
154+ sel . dataAttributes ?. start ,
155+ sel . dataAttributes ?. duration ,
156+ t ,
157+ ) ;
128158 const selector = selectorFromSelection ( sel ) ;
129159
130160 if ( ! selector ) {
@@ -135,19 +165,13 @@ export function useEnableKeyframes(
135165 if ( Object . keys ( position ) . length === 0 ) {
136166 position . x = 0 ;
137167 position . y = 0 ;
138- position . opacity = 1 ;
139168 }
140169
170+ // One keyframe at the playhead — a single diamond capturing the current
171+ // value. Motion comes from the user adding/dragging more keyframes later;
172+ // creating 0%+100% up front showed two diamonds for a single "add keyframe".
141173 const keyframes : Array < { percentage : number ; properties : Record < string , number | string > } > =
142174 [ { percentage : 0 , properties : { ...position } } ] ;
143- if ( pct > 1 && pct < 99 ) {
144- keyframes . push ( { percentage : pct , properties : { ...position } } ) ;
145- }
146- keyframes . push ( {
147- percentage : 100 ,
148- properties : { ...position } ,
149- auto : true ,
150- } as ( typeof keyframes ) [ number ] ) ;
151175
152176 if ( session . commitMutation ) {
153177 await session . commitMutation (
0 commit comments