@hyperframes/studio 0.2.4 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ *,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.\!container{width:100%!important}.container{width:100%}@media(min-width:640px){.\!container{max-width:640px!important}.container{max-width:640px}}@media(min-width:768px){.\!container{max-width:768px!important}.container{max-width:768px}}@media(min-width:1024px){.\!container{max-width:1024px!important}.container{max-width:1024px}}@media(min-width:1280px){.\!container{max-width:1280px!important}.container{max-width:1280px}}@media(min-width:1536px){.\!container{max-width:1536px!important}.container{max-width:1536px}}.pointer-events-none{pointer-events:none}.\!visible{visibility:visible!important}.visible{visibility:visible}.collapse{visibility:collapse}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{top:0;right:0;bottom:0;left:0}.bottom-0{bottom:0}.bottom-1{bottom:.25rem}.bottom-2{bottom:.5rem}.bottom-6{bottom:1.5rem}.bottom-full{bottom:100%}.left-0{left:0}.left-1\/2{left:50%}.right-0{right:0}.right-3{right:.75rem}.top-0{top:0}.top-1{top:.25rem}.top-1\/2{top:50%}.z-10{z-index:10}.z-20{z-index:20}.z-50{z-index:50}.z-\[100\]{z-index:100}.z-\[1\]{z-index:1}.z-\[200\]{z-index:200}.z-\[2\]{z-index:2}.z-\[90\]{z-index:90}.z-\[91\]{z-index:91}.mx-1{margin-left:.25rem;margin-right:.25rem}.my-0\.5{margin-top:.125rem;margin-bottom:.125rem}.my-1{margin-top:.25rem;margin-bottom:.25rem}.mb-0\.5{margin-bottom:.125rem}.mb-1{margin-bottom:.25rem}.mb-1\.5{margin-bottom:.375rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.ml-1\.5{margin-left:.375rem}.ml-auto{margin-left:auto}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-1\.5{margin-top:.375rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.hidden{display:none}.h-1{height:.25rem}.h-1\.5{height:.375rem}.h-10{height:2.5rem}.h-2{height:.5rem}.h-3{height:.75rem}.h-3\.5{height:.875rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-7{height:1.75rem}.h-8{height:2rem}.h-9{height:2.25rem}.h-\[1080px\]{height:1080px}.h-\[3px\]{height:3px}.h-\[45px\]{height:45px}.h-\[5px\]{height:5px}.h-full{height:100%}.h-screen{height:100vh}.max-h-24{max-height:6rem}.max-h-\[70\%\]{max-height:70%}.max-h-\[80vh\]{max-height:80vh}.max-h-full{max-height:100%}.min-h-0{min-height:0px}.min-h-7{min-height:1.75rem}.min-h-8{min-height:2rem}.min-h-9{min-height:2.25rem}.w-1{width:.25rem}.w-1\.5{width:.375rem}.w-14{width:3.5rem}.w-16{width:4rem}.w-2{width:.5rem}.w-20{width:5rem}.w-3{width:.75rem}.w-3\.5{width:.875rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-7{width:1.75rem}.w-8{width:2rem}.w-80{width:20rem}.w-\[160px\]{width:160px}.w-\[1920px\]{width:1920px}.w-full{width:100%}.w-px{width:1px}.w-screen{width:100vw}.min-w-0{min-width:0px}.min-w-7{min-width:1.75rem}.min-w-8{min-width:2rem}.min-w-9{min-width:2.25rem}.min-w-\[140px\]{min-width:140px}.min-w-\[160px\]{min-width:160px}.min-w-\[56px\]{min-width:56px}.min-w-\[72px\]{min-width:72px}.max-w-\[280px\]{max-width:280px}.max-w-full{max-width:100%}.max-w-xl{max-width:36rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.shrink{flex-shrink:1}.grow{flex-grow:1}.-translate-x-1\/2{--tw-translate-x: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-y-1\/2{--tw-translate-y: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.cursor-col-resize{cursor:col-resize}.cursor-crosshair{cursor:crosshair}.cursor-default{cursor:default}.cursor-pointer{cursor:pointer}.cursor-row-resize{cursor:row-resize}.select-none{-webkit-user-select:none;-moz-user-select:none;user-select:none}.resize-none{resize:none}.resize-y{resize:vertical}.resize{resize:both}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-0\.5{gap:.125rem}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-2\.5{gap:.625rem}.gap-3{gap:.75rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.overflow-x-hidden{overflow-x:hidden}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.rounded{border-radius:.25rem}.rounded-\[4px\]{border-radius:4px}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-xl{border-radius:.75rem}.rounded-l{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.rounded-r{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-l{border-left-width:1px}.border-l-2{border-left-width:2px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-none{border-style:none}.border-green-500\/30{border-color:#22c55e4d}.border-neutral-600{--tw-border-opacity: 1;border-color:rgb(82 82 82 / var(--tw-border-opacity, 1))}.border-neutral-700{--tw-border-opacity: 1;border-color:rgb(64 64 64 / var(--tw-border-opacity, 1))}.border-neutral-700\/20{border-color:#40404033}.border-neutral-700\/40{border-color:#40404066}.border-neutral-700\/50{border-color:#40404080}.border-neutral-700\/60{border-color:#40404099}.border-neutral-800{--tw-border-opacity: 1;border-color:rgb(38 38 38 / var(--tw-border-opacity, 1))}.border-neutral-800\/30{border-color:#2626264d}.border-neutral-800\/40{border-color:#26262666}.border-neutral-800\/50{border-color:#26262680}.border-neutral-800\/60{border-color:#26262699}.border-red-500{--tw-border-opacity: 1;border-color:rgb(239 68 68 / var(--tw-border-opacity, 1))}.border-red-700\/50{border-color:#b91c1c80}.border-studio-accent{--tw-border-opacity: 1;border-color:rgb(60 230 172 / var(--tw-border-opacity, 1))}.border-studio-accent\/25{border-color:#3ce6ac40}.border-studio-accent\/30{border-color:#3ce6ac4d}.border-studio-accent\/50{border-color:#3ce6ac80}.border-studio-accent\/60{border-color:#3ce6ac99}.border-transparent{border-color:transparent}.bg-\[\#0a0a0b\]{--tw-bg-opacity: 1;background-color:rgb(10 10 11 / var(--tw-bg-opacity, 1))}.bg-\[\#3CE6AC\]\/10{background-color:#3ce6ac1a}.bg-\[\#3CE6AC\]\/5{background-color:#3ce6ac0d}.bg-black{--tw-bg-opacity: 1;background-color:rgb(0 0 0 / var(--tw-bg-opacity, 1))}.bg-black\/50{background-color:#00000080}.bg-black\/60{background-color:#0009}.bg-green-500\/20{background-color:#22c55e33}.bg-green-600{--tw-bg-opacity: 1;background-color:rgb(22 163 74 / var(--tw-bg-opacity, 1))}.bg-neutral-600{--tw-bg-opacity: 1;background-color:rgb(82 82 82 / var(--tw-bg-opacity, 1))}.bg-neutral-600\/60{background-color:#52525299}.bg-neutral-700{--tw-bg-opacity: 1;background-color:rgb(64 64 64 / var(--tw-bg-opacity, 1))}.bg-neutral-700\/40{background-color:#40404066}.bg-neutral-800{--tw-bg-opacity: 1;background-color:rgb(38 38 38 / var(--tw-bg-opacity, 1))}.bg-neutral-800\/50{background-color:#26262680}.bg-neutral-800\/60{background-color:#26262699}.bg-neutral-900{--tw-bg-opacity: 1;background-color:rgb(23 23 23 / var(--tw-bg-opacity, 1))}.bg-neutral-900\/50{background-color:#17171780}.bg-neutral-950{--tw-bg-opacity: 1;background-color:rgb(10 10 10 / var(--tw-bg-opacity, 1))}.bg-red-400{--tw-bg-opacity: 1;background-color:rgb(248 113 113 / var(--tw-bg-opacity, 1))}.bg-red-500\/10{background-color:#ef44441a}.bg-red-600{--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity, 1))}.bg-red-900\/60{background-color:#7f1d1d99}.bg-red-900\/90{background-color:#7f1d1de6}.bg-red-950\/30{background-color:#450a0a4d}.bg-studio-accent{--tw-bg-opacity: 1;background-color:rgb(60 230 172 / var(--tw-bg-opacity, 1))}.bg-studio-accent\/10{background-color:#3ce6ac1a}.bg-studio-accent\/15{background-color:#3ce6ac26}.bg-studio-accent\/20{background-color:#3ce6ac33}.bg-studio-accent\/\[0\.03\]{background-color:#3ce6ac08}.bg-studio-accent\/\[0\.05\]{background-color:#3ce6ac0d}.bg-studio-accent\/\[0\.06\]{background-color:#3ce6ac0f}.bg-transparent{background-color:transparent}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.object-contain{-o-object-fit:contain;object-fit:contain}.object-cover{-o-object-fit:cover;object-fit:cover}.p-0\.5{padding:.125rem}.p-1{padding:.25rem}.p-1\.5{padding:.375rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-8{padding-left:2rem;padding-right:2rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb-0\.5{padding-bottom:.125rem}.pb-3{padding-bottom:.75rem}.pr-1\.5{padding-right:.375rem}.pt-3{padding-top:.75rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-\[13px\]{font-size:13px}.text-\[9px\]{font-size:9px}.text-base{font-size:1rem;line-height:1.5rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.lowercase{text-transform:lowercase}.capitalize{text-transform:capitalize}.italic{font-style:italic}.tabular-nums{--tw-numeric-spacing: tabular-nums;font-variant-numeric:var(--tw-ordinal) var(--tw-slashed-zero) var(--tw-numeric-figure) var(--tw-numeric-spacing) var(--tw-numeric-fraction)}.leading-none{line-height:1}.leading-relaxed{line-height:1.625}.leading-tight{line-height:1.25}.tracking-wider{letter-spacing:.05em}.text-\[\#09090B\]{--tw-text-opacity: 1;color:rgb(9 9 11 / var(--tw-text-opacity, 1))}.text-amber-400{--tw-text-opacity: 1;color:rgb(251 191 36 / var(--tw-text-opacity, 1))}.text-green-400{--tw-text-opacity: 1;color:rgb(74 222 128 / var(--tw-text-opacity, 1))}.text-neutral-100{--tw-text-opacity: 1;color:rgb(245 245 245 / var(--tw-text-opacity, 1))}.text-neutral-200{--tw-text-opacity: 1;color:rgb(229 229 229 / var(--tw-text-opacity, 1))}.text-neutral-300{--tw-text-opacity: 1;color:rgb(212 212 212 / var(--tw-text-opacity, 1))}.text-neutral-400{--tw-text-opacity: 1;color:rgb(163 163 163 / var(--tw-text-opacity, 1))}.text-neutral-500{--tw-text-opacity: 1;color:rgb(115 115 115 / var(--tw-text-opacity, 1))}.text-neutral-600{--tw-text-opacity: 1;color:rgb(82 82 82 / var(--tw-text-opacity, 1))}.text-neutral-700{--tw-text-opacity: 1;color:rgb(64 64 64 / var(--tw-text-opacity, 1))}.text-neutral-950{--tw-text-opacity: 1;color:rgb(10 10 10 / var(--tw-text-opacity, 1))}.text-purple-400{--tw-text-opacity: 1;color:rgb(192 132 252 / var(--tw-text-opacity, 1))}.text-red-200{--tw-text-opacity: 1;color:rgb(254 202 202 / var(--tw-text-opacity, 1))}.text-red-300{--tw-text-opacity: 1;color:rgb(252 165 165 / var(--tw-text-opacity, 1))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.text-studio-accent{--tw-text-opacity: 1;color:rgb(60 230 172 / var(--tw-text-opacity, 1))}.text-studio-accent\/50{color:#3ce6ac80}.text-studio-accent\/60{color:#3ce6ac99}.text-studio-accent\/80{color:#3ce6accc}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.underline{text-decoration-line:underline}.line-through{text-decoration-line:line-through}.accent-studio-accent{accent-color:#3CE6AC}.opacity-25{opacity:.25}.opacity-70{opacity:.7}.opacity-75{opacity:.75}.shadow{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-2xl{--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / .1), 0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-black\/40{--tw-shadow-color: rgb(0 0 0 / .4);--tw-shadow: var(--tw-shadow-colored)}.outline-none{outline:2px solid transparent;outline-offset:2px}.outline{outline-style:solid}.outline-1{outline-width:1px}.-outline-offset-1{outline-offset:-1px}.outline-\[\#3CE6AC\]\/30{outline-color:#3ce6ac4d}.outline-\[\#3CE6AC\]\/40{outline-color:#3ce6ac66}.ring{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-1{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-2{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-studio-accent{--tw-ring-opacity: 1;--tw-ring-color: rgb(60 230 172 / var(--tw-ring-opacity, 1))}.ring-white\/50{--tw-ring-color: rgb(255 255 255 / .5)}.blur{--tw-blur: blur(8px);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.drop-shadow{--tw-drop-shadow: drop-shadow(0 1px 2px rgb(0 0 0 / .1)) drop-shadow(0 1px 1px rgb(0 0 0 / .06));filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.invert{--tw-invert: invert(100%);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-sm{--tw-backdrop-blur: blur(4px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-150{transition-duration:.15s}.duration-300{transition-duration:.3s}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}body{margin:0;padding:0;background:#0a0a0a;color:#e5e5e5;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;overflow:hidden}#root{width:100vw;height:100vh}.cm-editor{height:100%;font-size:13px}.cm-editor .cm-scroller{font-family:JetBrains Mono,Fira Code,SF Mono,monospace}.cm-editor.cm-focused{outline:none}.placeholder\:text-neutral-600::-moz-placeholder{--tw-text-opacity: 1;color:rgb(82 82 82 / var(--tw-text-opacity, 1))}.placeholder\:text-neutral-600::placeholder{--tw-text-opacity: 1;color:rgb(82 82 82 / var(--tw-text-opacity, 1))}.last\:border-0:last-child{border-width:0px}.hover\:border-neutral-600:hover{--tw-border-opacity: 1;border-color:rgb(82 82 82 / var(--tw-border-opacity, 1))}.hover\:border-studio-accent\/50:hover{border-color:#3ce6ac80}.hover\:bg-neutral-200:hover{--tw-bg-opacity: 1;background-color:rgb(229 229 229 / var(--tw-bg-opacity, 1))}.hover\:bg-neutral-600:hover{--tw-bg-opacity: 1;background-color:rgb(82 82 82 / var(--tw-bg-opacity, 1))}.hover\:bg-neutral-800:hover{--tw-bg-opacity: 1;background-color:rgb(38 38 38 / var(--tw-bg-opacity, 1))}.hover\:bg-neutral-800\/30:hover{background-color:#2626264d}.hover\:bg-neutral-800\/50:hover{background-color:#26262680}.hover\:bg-red-500:hover{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity, 1))}.hover\:bg-red-600:hover{--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity, 1))}.hover\:bg-red-800\/60:hover{background-color:#991b1b99}.hover\:bg-red-900\/30:hover{background-color:#7f1d1d4d}.hover\:bg-studio-accent:hover{--tw-bg-opacity: 1;background-color:rgb(60 230 172 / var(--tw-bg-opacity, 1))}.hover\:bg-studio-accent\/25:hover{background-color:#3ce6ac40}.hover\:bg-studio-accent\/80:hover{background-color:#3ce6accc}.hover\:text-amber-300:hover{--tw-text-opacity: 1;color:rgb(252 211 77 / var(--tw-text-opacity, 1))}.hover\:text-green-400:hover{--tw-text-opacity: 1;color:rgb(74 222 128 / var(--tw-text-opacity, 1))}.hover\:text-neutral-200:hover{--tw-text-opacity: 1;color:rgb(229 229 229 / var(--tw-text-opacity, 1))}.hover\:text-neutral-300:hover{--tw-text-opacity: 1;color:rgb(212 212 212 / var(--tw-text-opacity, 1))}.hover\:text-neutral-400:hover{--tw-text-opacity: 1;color:rgb(163 163 163 / var(--tw-text-opacity, 1))}.hover\:text-red-400:hover{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.hover\:text-studio-accent:hover{--tw-text-opacity: 1;color:rgb(60 230 172 / var(--tw-text-opacity, 1))}.hover\:text-white:hover{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.hover\:ring-1:hover{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.hover\:ring-white\/30:hover{--tw-ring-color: rgb(255 255 255 / .3)}.hover\:brightness-110:hover{--tw-brightness: brightness(1.1);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.focus\:border-\[\#3CE6AC\]:focus{--tw-border-opacity: 1;border-color:rgb(60 230 172 / var(--tw-border-opacity, 1))}.focus\:border-neutral-600:focus{--tw-border-opacity: 1;border-color:rgb(82 82 82 / var(--tw-border-opacity, 1))}.focus\:border-studio-accent:focus{--tw-border-opacity: 1;border-color:rgb(60 230 172 / var(--tw-border-opacity, 1))}.focus\:border-studio-accent\/40:focus{border-color:#3ce6ac66}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.active\:scale-\[0\.97\]:active{--tw-scale-x: .97;--tw-scale-y: .97;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.active\:scale-\[0\.98\]:active{--tw-scale-x: .98;--tw-scale-y: .98;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.active\:bg-studio-accent\/80:active{background-color:#3ce6accc}.disabled\:pointer-events-none:disabled{pointer-events:none}.disabled\:opacity-30:disabled{opacity:.3}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:scale-125{--tw-scale-x: 1.25;--tw-scale-y: 1.25;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}
package/dist/index.html CHANGED
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>HyperFrames Studio</title>
7
- <script type="module" crossorigin src="/assets/index-B0VCLOXQ.js"></script>
8
- <link rel="stylesheet" crossorigin href="/assets/index-BT9D8I7B.css">
7
+ <script type="module" crossorigin src="/assets/index-ByOno-cZ.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/assets/index-DHr9yo58.css">
9
9
  </head>
10
10
  <body>
11
11
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/studio",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
@@ -18,6 +18,13 @@
18
18
  ".": "./src/index.ts",
19
19
  "./tailwind-preset": "./src/styles/tailwind-preset.ts"
20
20
  },
21
+ "scripts": {
22
+ "dev": "vite",
23
+ "build": "vite build",
24
+ "typecheck": "tsc --noEmit",
25
+ "test": "vitest run",
26
+ "test:watch": "vitest"
27
+ },
21
28
  "dependencies": {
22
29
  "@codemirror/autocomplete": "^6.20.1",
23
30
  "@codemirror/commands": "^6.10.3",
@@ -29,12 +36,14 @@
29
36
  "@codemirror/state": "^6.6.0",
30
37
  "@codemirror/theme-one-dark": "^6.1.2",
31
38
  "@codemirror/view": "^6.40.0",
39
+ "@hyperframes/core": "workspace:*",
40
+ "@hyperframes/player": "workspace:*",
32
41
  "@phosphor-icons/react": "^2.1.10",
33
42
  "codemirror": "^6.0.1",
34
- "motion": "^12.38.0",
35
- "@hyperframes/core": "0.2.4"
43
+ "motion": "^12.38.0"
36
44
  },
37
45
  "devDependencies": {
46
+ "@hyperframes/producer": "workspace:*",
38
47
  "@types/react": "^19.0.0",
39
48
  "@types/react-dom": "^19.0.0",
40
49
  "@vitejs/plugin-react": "^4.0.0",
@@ -43,21 +52,13 @@
43
52
  "puppeteer-core": "^24.40.0",
44
53
  "tailwindcss": "^3.4.0",
45
54
  "typescript": "^5.0.0",
46
- "vite": "^5.0.0",
55
+ "vite": "^6.4.2",
47
56
  "vitest": "^3.2.4",
48
- "zustand": "^5.0.0",
49
- "@hyperframes/producer": "0.2.4"
57
+ "zustand": "^5.0.0"
50
58
  },
51
59
  "peerDependencies": {
52
60
  "react": "^18.0.0 || ^19.0.0",
53
61
  "react-dom": "^18.0.0 || ^19.0.0",
54
62
  "zustand": "^4.0.0 || ^5.0.0"
55
- },
56
- "scripts": {
57
- "dev": "vite",
58
- "build": "vite build",
59
- "typecheck": "tsc --noEmit",
60
- "test": "vitest run",
61
- "test:watch": "vitest"
62
63
  }
63
- }
64
+ }
package/src/index.ts CHANGED
@@ -12,6 +12,7 @@ export {
12
12
  VideoThumbnail,
13
13
  CompositionThumbnail,
14
14
  useTimelinePlayer,
15
+ resolveIframe,
15
16
  usePlayerStore,
16
17
  liveTime,
17
18
  formatTime,
@@ -1,8 +1,7 @@
1
- import { forwardRef, useRef, useState, useCallback } from "react";
1
+ import { forwardRef, useRef } from "react";
2
2
  import { useMountEffect } from "../../hooks/useMountEffect";
3
-
4
- const NATIVE_W = 1920;
5
- const NATIVE_H = 1080;
3
+ import type { HyperframesPlayer } from "@hyperframes/player";
4
+ import "@hyperframes/player"; // registers <hyperframes-player> custom element
6
5
 
7
6
  interface PlayerProps {
8
7
  projectId?: string;
@@ -11,118 +10,80 @@ interface PlayerProps {
11
10
  portrait?: boolean;
12
11
  }
13
12
 
13
+ /**
14
+ * Renders a composition preview using the <hyperframes-player> web component.
15
+ *
16
+ * The web component handles iframe scaling, dimension detection, and
17
+ * ResizeObserver internally. This wrapper bridges its inner iframe to the
18
+ * forwarded ref so useTimelinePlayer can access it for clip manifest parsing,
19
+ * timeline probing, and DOM inspection.
20
+ */
14
21
  export const Player = forwardRef<HTMLIFrameElement, PlayerProps>(
15
22
  ({ projectId, directUrl, onLoad, portrait }, ref) => {
16
23
  const containerRef = useRef<HTMLDivElement>(null);
17
- const [scale, setScale] = useState(1);
18
- const dimsRef = useRef({
19
- w: portrait ? NATIVE_H : NATIVE_W,
20
- h: portrait ? NATIVE_W : NATIVE_H,
21
- });
22
- const [dims, setDims] = useState(dimsRef.current);
23
24
  const loadCountRef = useRef(0);
24
25
 
25
- const updateScale = useCallback(() => {
26
- const el = containerRef.current;
27
- if (!el) return;
28
- const rect = el.getBoundingClientRect();
29
- const d = dimsRef.current;
30
- setScale(Math.min(rect.width / d.w, rect.height / d.h));
31
- }, []);
32
-
33
26
  useMountEffect(() => {
34
- updateScale();
35
- const ro = new ResizeObserver(updateScale);
36
- if (containerRef.current) ro.observe(containerRef.current);
27
+ const container = containerRef.current;
28
+ if (!container) return;
37
29
 
38
- // Listen for stage-size messages from the runtime
39
- const handleMessage = (e: MessageEvent) => {
40
- const data = e.data;
41
- if (
42
- data?.source === "hf-preview" &&
43
- data?.type === "stage-size" &&
44
- data.width > 0 &&
45
- data.height > 0
46
- ) {
47
- if (dimsRef.current.w !== data.width || dimsRef.current.h !== data.height) {
48
- dimsRef.current = { w: data.width, h: data.height };
49
- setDims(dimsRef.current);
50
- updateScale();
51
- }
52
- }
53
- };
54
- window.addEventListener("message", handleMessage);
30
+ // Create the web component imperatively to avoid JSX custom-element typing.
31
+ const player = document.createElement("hyperframes-player") as HyperframesPlayer;
32
+ const src = directUrl || `/api/projects/${projectId}/preview`;
33
+ player.setAttribute("src", src);
34
+ player.setAttribute("width", String(portrait ? 1080 : 1920));
35
+ player.setAttribute("height", String(portrait ? 1920 : 1080));
36
+ player.style.width = "100%";
37
+ player.style.height = "100%";
38
+ player.style.display = "block";
39
+ container.appendChild(player);
55
40
 
56
- return () => {
57
- ro.disconnect();
58
- window.removeEventListener("message", handleMessage);
59
- };
60
- });
41
+ // Bridge the inner iframe to the forwarded ref for useTimelinePlayer.
42
+ const iframe = player.iframeElement;
43
+ if (typeof ref === "function") {
44
+ ref(iframe);
45
+ } else if (ref) {
46
+ (ref as React.MutableRefObject<HTMLIFrameElement | null>).current = iframe;
47
+ }
61
48
 
62
- const handleLoad = useCallback(() => {
63
- loadCountRef.current++;
49
+ // Prevent the web component's built-in click-to-toggle behavior.
50
+ // The studio manages playback exclusively via useTimelinePlayer.
51
+ const preventToggle = (e: Event) => e.stopImmediatePropagation();
52
+ player.addEventListener("click", preventToggle, { capture: true });
64
53
 
65
- // Auto-detect dimensions from the composition's data-width/data-height
66
- try {
67
- const iframeEl = typeof ref === "function" ? null : ref?.current;
68
- const doc = iframeEl?.contentDocument;
69
- if (doc) {
70
- const root = doc.querySelector("[data-composition-id]");
71
- if (root) {
72
- const dw = parseInt(root.getAttribute("data-width") || "0", 10);
73
- const dh = parseInt(root.getAttribute("data-height") || "0", 10);
74
- if (dw > 0 && dh > 0 && (dw !== dimsRef.current.w || dh !== dimsRef.current.h)) {
75
- dimsRef.current = { w: dw, h: dh };
76
- setDims(dimsRef.current);
77
- // Recalc scale with new dims
78
- const el = containerRef.current;
79
- if (el) {
80
- const rect = el.getBoundingClientRect();
81
- setScale(Math.min(rect.width / dw, rect.height / dh));
82
- }
83
- }
84
- }
54
+ // Forward the iframe's native load event to the studio's onIframeLoad.
55
+ const handleLoad = () => {
56
+ loadCountRef.current++;
57
+ // Reveal animation on reload (hot-reload, composition switch)
58
+ if (loadCountRef.current > 1) {
59
+ container.classList.remove("preview-revealing");
60
+ void container.offsetWidth;
61
+ container.classList.add("preview-revealing");
62
+ const onEnd = () => container.classList.remove("preview-revealing");
63
+ container.addEventListener("animationend", onEnd, { once: true });
85
64
  }
86
- } catch (err) {
87
- console.warn("[Player] Could not read iframe dimensions (cross-origin)", err);
88
- }
65
+ onLoad();
66
+ };
67
+ iframe.addEventListener("load", handleLoad);
89
68
 
90
- if (loadCountRef.current > 1) {
91
- const el = containerRef.current;
92
- if (el) {
93
- el.classList.remove("preview-revealing");
94
- void el.offsetWidth;
95
- el.classList.add("preview-revealing");
96
- const onEnd = () => el.classList.remove("preview-revealing");
97
- el.addEventListener("animationend", onEnd, { once: true });
69
+ return () => {
70
+ iframe.removeEventListener("load", handleLoad);
71
+ player.removeEventListener("click", preventToggle, { capture: true });
72
+ container.removeChild(player);
73
+ // Clear the forwarded ref
74
+ if (typeof ref === "function") {
75
+ ref(null);
76
+ } else if (ref) {
77
+ (ref as React.MutableRefObject<HTMLIFrameElement | null>).current = null;
98
78
  }
99
- }
100
- onLoad();
101
- }, [onLoad, ref]);
79
+ };
80
+ });
102
81
 
103
82
  return (
104
83
  <div
105
84
  ref={containerRef}
106
85
  className="w-full h-full max-w-full max-h-full overflow-hidden bg-black flex items-center justify-center"
107
- >
108
- <iframe
109
- ref={ref}
110
- src={directUrl || `/api/projects/${projectId}/preview`}
111
- onLoad={handleLoad}
112
- sandbox="allow-scripts allow-same-origin"
113
- allow="autoplay; fullscreen"
114
- referrerPolicy="no-referrer"
115
- title="Project Preview"
116
- style={{
117
- width: dims.w,
118
- height: dims.h,
119
- border: "none",
120
- transform: `scale(${scale})`,
121
- transformOrigin: "center center",
122
- flexShrink: 0,
123
- }}
124
- />
125
- </div>
86
+ />
126
87
  );
127
88
  },
128
89
  );
@@ -186,8 +186,34 @@ function unmutePreviewMedia(iframe: HTMLIFrameElement | null): void {
186
186
  }
187
187
  }
188
188
 
189
+ /**
190
+ * Resolve the underlying iframe from any host element. Supports:
191
+ * - Direct `<iframe>` element (most common — studio's own `Player.tsx`)
192
+ * - Custom elements (e.g. `<hyperframes-player>`) whose shadow DOM contains an iframe
193
+ * - Wrapper elements whose light DOM contains a descendant iframe
194
+ *
195
+ * Exported so web-component consumers can pre-resolve the iframe before
196
+ * assigning it to `iframeRef` returned by `useTimelinePlayer`. Returns `null`
197
+ * when the element has no associated iframe yet.
198
+ *
199
+ * @example
200
+ * ```tsx
201
+ * const { iframeRef } = useTimelinePlayer();
202
+ * const playerElRef = useRef<HyperframesPlayer>(null);
203
+ *
204
+ * useEffect(() => {
205
+ * iframeRef.current = resolveIframe(playerElRef.current);
206
+ * }, [iframeRef]);
207
+ * ```
208
+ */
209
+ export function resolveIframe(el: Element | null): HTMLIFrameElement | null {
210
+ if (!el) return null;
211
+ if (el instanceof HTMLIFrameElement) return el;
212
+ return el.shadowRoot?.querySelector("iframe") ?? el.querySelector("iframe") ?? null;
213
+ }
214
+
189
215
  export function useTimelinePlayer() {
190
- const iframeRef = useRef<HTMLIFrameElement>(null);
216
+ const iframeRef = useRef<HTMLIFrameElement | null>(null);
191
217
  const rafRef = useRef<number>(0);
192
218
  const probeIntervalRef = useRef<ReturnType<typeof setInterval> | undefined>(undefined);
193
219
  const pendingSeekRef = useRef<number | null>(null);
@@ -200,7 +226,8 @@ export function useTimelinePlayer() {
200
226
 
201
227
  const getAdapter = useCallback((): PlaybackAdapter | null => {
202
228
  try {
203
- const win = iframeRef.current?.contentWindow as IframeWindow | null;
229
+ const iframe = iframeRef.current;
230
+ const win = iframe?.contentWindow as IframeWindow | null;
204
231
  if (!win) return null;
205
232
 
206
233
  if (win.__player && typeof win.__player.play === "function") {
@@ -211,7 +238,17 @@ export function useTimelinePlayer() {
211
238
 
212
239
  if (win.__timelines) {
213
240
  const keys = Object.keys(win.__timelines);
214
- if (keys.length > 0) return wrapTimeline(win.__timelines[keys[keys.length - 1]]);
241
+ if (keys.length > 0) {
242
+ // Resolve the root composition id from the DOM — the outermost
243
+ // `[data-composition-id]` element is the master. Without this,
244
+ // Object.keys() order would let a sub-composition's timeline
245
+ // hijack play/pause/seek and the duration readout.
246
+ const rootId = iframe?.contentDocument
247
+ ?.querySelector("[data-composition-id]")
248
+ ?.getAttribute("data-composition-id");
249
+ const key = rootId && rootId in win.__timelines ? rootId : keys[keys.length - 1];
250
+ return wrapTimeline(win.__timelines[key]);
251
+ }
215
252
  }
216
253
 
217
254
  return null;
@@ -554,8 +591,9 @@ export function useTimelinePlayer() {
554
591
  setIsPlaying(false);
555
592
 
556
593
  try {
557
- const doc = iframeRef.current?.contentDocument;
558
- const iframeWin = iframeRef.current?.contentWindow as IframeWindow | null;
594
+ const iframe = iframeRef.current;
595
+ const doc = iframe?.contentDocument;
596
+ const iframeWin = iframe?.contentWindow as IframeWindow | null;
559
597
  if (doc && iframeWin) {
560
598
  normalizePreviewViewport(doc, iframeWin);
561
599
  autoHealMissingCompositionIds(doc);
@@ -591,7 +629,7 @@ export function useTimelinePlayer() {
591
629
  const rootId = rootComp.getAttribute("data-composition-id") || "composition";
592
630
  // Derive compositionSrc from the iframe URL for thumbnail rendering.
593
631
  // URL pattern: /api/projects/{id}/preview/comp/{path}
594
- const iframeSrc = iframeRef.current?.src || "";
632
+ const iframeSrc = iframe?.src || "";
595
633
  const compPathMatch = iframeSrc.match(/\/preview\/comp\/(.+?)(?:\?|$)/);
596
634
  const compositionSrc = compPathMatch
597
635
  ? decodeURIComponent(compPathMatch[1])
@@ -682,7 +720,8 @@ export function useTimelinePlayer() {
682
720
  const handleMessage = (e: MessageEvent) => {
683
721
  const data = e.data;
684
722
  // Only process messages from the main preview iframe — ignore MediaPanel/ClipThumbnail iframes
685
- if (e.source && iframeRef.current && e.source !== iframeRef.current.contentWindow) {
723
+ const ourIframe = iframeRef.current;
724
+ if (e.source && ourIframe && e.source !== ourIframe.contentWindow) {
686
725
  return;
687
726
  }
688
727
  // Also handle the runtime's state message which includes timeline data
@@ -690,8 +729,7 @@ export function useTimelinePlayer() {
690
729
  // State message means the runtime is alive — check for elements
691
730
  try {
692
731
  if (usePlayerStore.getState().elements.length === 0) {
693
- const iframe = iframeRef.current;
694
- const iframeWin = iframe?.contentWindow as IframeWindow | null;
732
+ const iframeWin = ourIframe?.contentWindow as IframeWindow | null;
695
733
  const manifest = iframeWin?.__clipManifest;
696
734
  if (manifest && manifest.clips.length > 0) {
697
735
  processTimelineMessageRef.current(manifest);
@@ -717,8 +755,7 @@ export function useTimelinePlayer() {
717
755
  // If manifest produced 0 elements after filtering, try DOM fallback
718
756
  if (usePlayerStore.getState().elements.length === 0) {
719
757
  try {
720
- const iframe = iframeRef.current;
721
- const doc = iframe?.contentDocument;
758
+ const doc = ourIframe?.contentDocument;
722
759
  const adapter = getAdapter();
723
760
  if (doc && adapter) {
724
761
  const els = parseTimelineFromDOM(doc, adapter.getDuration());
@@ -6,7 +6,7 @@ export { VideoThumbnail } from "./components/VideoThumbnail";
6
6
  export { CompositionThumbnail } from "./components/CompositionThumbnail";
7
7
 
8
8
  // Hooks
9
- export { useTimelinePlayer } from "./hooks/useTimelinePlayer";
9
+ export { useTimelinePlayer, resolveIframe } from "./hooks/useTimelinePlayer";
10
10
 
11
11
  // Store
12
12
  export { usePlayerStore, liveTime } from "./store/playerStore";
@@ -112,13 +112,13 @@ describe("usePlayerStore", () => {
112
112
  });
113
113
  });
114
114
 
115
- describe("updateElementStart", () => {
115
+ describe("updateElement", () => {
116
116
  it("updates the start time of a specific element", () => {
117
117
  usePlayerStore.getState().setElements([
118
118
  { id: "el-1", tag: "div", start: 0, duration: 5, track: 0 },
119
119
  { id: "el-2", tag: "div", start: 5, duration: 5, track: 1 },
120
120
  ]);
121
- usePlayerStore.getState().updateElementStart("el-1", 3);
121
+ usePlayerStore.getState().updateElement("el-1", { start: 3 });
122
122
  const elements = usePlayerStore.getState().elements;
123
123
  expect(elements[0].start).toBe(3);
124
124
  expect(elements[1].start).toBe(5); // unchanged
@@ -129,7 +129,7 @@ describe("usePlayerStore", () => {
129
129
  { id: "el-1", tag: "div", start: 0, duration: 5, track: 0 },
130
130
  ];
131
131
  usePlayerStore.getState().setElements(original);
132
- usePlayerStore.getState().updateElementStart("nonexistent", 10);
132
+ usePlayerStore.getState().updateElement("nonexistent", { start: 10 });
133
133
  expect(usePlayerStore.getState().elements[0].start).toBe(0);
134
134
  });
135
135
  });