@radix-ng/primitives 0.51.0 → 1.0.0-beta.1

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.
Files changed (186) hide show
  1. package/fesm2022/radix-ng-primitives-accordion.mjs +105 -38
  2. package/fesm2022/radix-ng-primitives-accordion.mjs.map +1 -1
  3. package/fesm2022/radix-ng-primitives-alert-dialog.mjs +221 -129
  4. package/fesm2022/radix-ng-primitives-alert-dialog.mjs.map +1 -1
  5. package/fesm2022/radix-ng-primitives-arrow.mjs +20 -4
  6. package/fesm2022/radix-ng-primitives-arrow.mjs.map +1 -1
  7. package/fesm2022/radix-ng-primitives-aspect-ratio.mjs.map +1 -1
  8. package/fesm2022/radix-ng-primitives-avatar.mjs +54 -61
  9. package/fesm2022/radix-ng-primitives-avatar.mjs.map +1 -1
  10. package/fesm2022/radix-ng-primitives-button.mjs +123 -0
  11. package/fesm2022/radix-ng-primitives-button.mjs.map +1 -0
  12. package/fesm2022/radix-ng-primitives-calendar.mjs +95 -83
  13. package/fesm2022/radix-ng-primitives-calendar.mjs.map +1 -1
  14. package/fesm2022/radix-ng-primitives-checkbox.mjs +378 -54
  15. package/fesm2022/radix-ng-primitives-checkbox.mjs.map +1 -1
  16. package/fesm2022/radix-ng-primitives-collapsible.mjs +182 -81
  17. package/fesm2022/radix-ng-primitives-collapsible.mjs.map +1 -1
  18. package/fesm2022/radix-ng-primitives-collection.mjs +40 -57
  19. package/fesm2022/radix-ng-primitives-collection.mjs.map +1 -1
  20. package/fesm2022/radix-ng-primitives-config.mjs.map +1 -1
  21. package/fesm2022/radix-ng-primitives-context-menu.mjs +140 -424
  22. package/fesm2022/radix-ng-primitives-context-menu.mjs.map +1 -1
  23. package/fesm2022/radix-ng-primitives-core.mjs +845 -744
  24. package/fesm2022/radix-ng-primitives-core.mjs.map +1 -1
  25. package/fesm2022/radix-ng-primitives-cropper.mjs +288 -308
  26. package/fesm2022/radix-ng-primitives-cropper.mjs.map +1 -1
  27. package/fesm2022/radix-ng-primitives-date-field.mjs +104 -58
  28. package/fesm2022/radix-ng-primitives-date-field.mjs.map +1 -1
  29. package/fesm2022/radix-ng-primitives-dialog.mjs +655 -327
  30. package/fesm2022/radix-ng-primitives-dialog.mjs.map +1 -1
  31. package/fesm2022/radix-ng-primitives-dismissable-layer.mjs +70 -46
  32. package/fesm2022/radix-ng-primitives-dismissable-layer.mjs.map +1 -1
  33. package/fesm2022/radix-ng-primitives-drawer.mjs +960 -0
  34. package/fesm2022/radix-ng-primitives-drawer.mjs.map +1 -0
  35. package/fesm2022/radix-ng-primitives-editable.mjs +304 -23
  36. package/fesm2022/radix-ng-primitives-editable.mjs.map +1 -1
  37. package/fesm2022/radix-ng-primitives-field.mjs +363 -0
  38. package/fesm2022/radix-ng-primitives-field.mjs.map +1 -0
  39. package/fesm2022/radix-ng-primitives-fieldset.mjs +79 -0
  40. package/fesm2022/radix-ng-primitives-fieldset.mjs.map +1 -0
  41. package/fesm2022/radix-ng-primitives-focus-scope.mjs +23 -8
  42. package/fesm2022/radix-ng-primitives-focus-scope.mjs.map +1 -1
  43. package/fesm2022/radix-ng-primitives-input.mjs +172 -0
  44. package/fesm2022/radix-ng-primitives-input.mjs.map +1 -0
  45. package/fesm2022/radix-ng-primitives-label.mjs +6 -6
  46. package/fesm2022/radix-ng-primitives-label.mjs.map +1 -1
  47. package/fesm2022/radix-ng-primitives-menu.mjs +1907 -363
  48. package/fesm2022/radix-ng-primitives-menu.mjs.map +1 -1
  49. package/fesm2022/radix-ng-primitives-menubar.mjs +290 -162
  50. package/fesm2022/radix-ng-primitives-menubar.mjs.map +1 -1
  51. package/fesm2022/radix-ng-primitives-meter.mjs +271 -0
  52. package/fesm2022/radix-ng-primitives-meter.mjs.map +1 -0
  53. package/fesm2022/radix-ng-primitives-navigation-menu.mjs +1052 -1553
  54. package/fesm2022/radix-ng-primitives-navigation-menu.mjs.map +1 -1
  55. package/fesm2022/radix-ng-primitives-number-field.mjs +1102 -367
  56. package/fesm2022/radix-ng-primitives-number-field.mjs.map +1 -1
  57. package/fesm2022/radix-ng-primitives-pagination.mjs.map +1 -1
  58. package/fesm2022/radix-ng-primitives-popover.mjs +978 -989
  59. package/fesm2022/radix-ng-primitives-popover.mjs.map +1 -1
  60. package/fesm2022/radix-ng-primitives-popper.mjs +111 -44
  61. package/fesm2022/radix-ng-primitives-popper.mjs.map +1 -1
  62. package/fesm2022/radix-ng-primitives-portal.mjs +34 -10
  63. package/fesm2022/radix-ng-primitives-portal.mjs.map +1 -1
  64. package/fesm2022/radix-ng-primitives-presence.mjs +134 -246
  65. package/fesm2022/radix-ng-primitives-presence.mjs.map +1 -1
  66. package/fesm2022/radix-ng-primitives-preview-card.mjs +997 -0
  67. package/fesm2022/radix-ng-primitives-preview-card.mjs.map +1 -0
  68. package/fesm2022/radix-ng-primitives-progress.mjs +223 -84
  69. package/fesm2022/radix-ng-primitives-progress.mjs.map +1 -1
  70. package/fesm2022/radix-ng-primitives-radio.mjs +191 -51
  71. package/fesm2022/radix-ng-primitives-radio.mjs.map +1 -1
  72. package/fesm2022/radix-ng-primitives-roving-focus.mjs +96 -50
  73. package/fesm2022/radix-ng-primitives-roving-focus.mjs.map +1 -1
  74. package/fesm2022/radix-ng-primitives-scroll-area.mjs +923 -0
  75. package/fesm2022/radix-ng-primitives-scroll-area.mjs.map +1 -0
  76. package/fesm2022/radix-ng-primitives-select.mjs +791 -509
  77. package/fesm2022/radix-ng-primitives-select.mjs.map +1 -1
  78. package/fesm2022/radix-ng-primitives-separator.mjs +12 -35
  79. package/fesm2022/radix-ng-primitives-separator.mjs.map +1 -1
  80. package/fesm2022/radix-ng-primitives-slider.mjs +969 -717
  81. package/fesm2022/radix-ng-primitives-slider.mjs.map +1 -1
  82. package/fesm2022/radix-ng-primitives-stepper.mjs +15 -19
  83. package/fesm2022/radix-ng-primitives-stepper.mjs.map +1 -1
  84. package/fesm2022/radix-ng-primitives-switch.mjs +125 -113
  85. package/fesm2022/radix-ng-primitives-switch.mjs.map +1 -1
  86. package/fesm2022/radix-ng-primitives-tabs.mjs +390 -108
  87. package/fesm2022/radix-ng-primitives-tabs.mjs.map +1 -1
  88. package/fesm2022/radix-ng-primitives-time-field.mjs +55 -46
  89. package/fesm2022/radix-ng-primitives-time-field.mjs.map +1 -1
  90. package/fesm2022/radix-ng-primitives-toast.mjs +839 -0
  91. package/fesm2022/radix-ng-primitives-toast.mjs.map +1 -0
  92. package/fesm2022/radix-ng-primitives-toggle-group.mjs +121 -247
  93. package/fesm2022/radix-ng-primitives-toggle-group.mjs.map +1 -1
  94. package/fesm2022/radix-ng-primitives-toggle.mjs +98 -61
  95. package/fesm2022/radix-ng-primitives-toggle.mjs.map +1 -1
  96. package/fesm2022/radix-ng-primitives-toolbar.mjs +303 -92
  97. package/fesm2022/radix-ng-primitives-toolbar.mjs.map +1 -1
  98. package/fesm2022/radix-ng-primitives-tooltip.mjs +699 -1072
  99. package/fesm2022/radix-ng-primitives-tooltip.mjs.map +1 -1
  100. package/fesm2022/radix-ng-primitives-visually-hidden.mjs +25 -66
  101. package/fesm2022/radix-ng-primitives-visually-hidden.mjs.map +1 -1
  102. package/meter/README.md +3 -0
  103. package/navigation-menu/README.md +2 -1
  104. package/package.json +39 -18
  105. package/portal/README.md +2 -0
  106. package/preview-card/README.md +3 -0
  107. package/schematics/collection.json +1 -0
  108. package/schematics/ng-add/index.d.ts +3 -2
  109. package/schematics/ng-add/index.js +62 -31
  110. package/schematics/ng-add/index.js.map +1 -1
  111. package/schematics/ng-add/package-config.d.ts +4 -2
  112. package/schematics/ng-add/package-config.js +10 -2
  113. package/schematics/ng-add/package-config.js.map +1 -1
  114. package/schematics/ng-add/schema.d.ts +3 -0
  115. package/schematics/ng-add/schema.js +3 -0
  116. package/schematics/ng-add/schema.js.map +1 -0
  117. package/schematics/ng-add/schema.json +14 -0
  118. package/select/README.md +2 -0
  119. package/types/radix-ng-primitives-accordion.d.ts +51 -16
  120. package/types/radix-ng-primitives-alert-dialog.d.ts +95 -38
  121. package/types/radix-ng-primitives-arrow.d.ts +1 -1
  122. package/types/radix-ng-primitives-aspect-ratio.d.ts +1 -1
  123. package/types/radix-ng-primitives-avatar.d.ts +7 -11
  124. package/types/radix-ng-primitives-button.d.ts +73 -0
  125. package/types/radix-ng-primitives-calendar.d.ts +39 -20
  126. package/types/radix-ng-primitives-checkbox.d.ts +204 -35
  127. package/types/radix-ng-primitives-collapsible.d.ts +114 -40
  128. package/types/radix-ng-primitives-collection.d.ts +38 -34
  129. package/types/radix-ng-primitives-config.d.ts +1 -1
  130. package/types/radix-ng-primitives-context-menu.d.ts +61 -116
  131. package/types/radix-ng-primitives-core.d.ts +345 -235
  132. package/types/radix-ng-primitives-cropper.d.ts +89 -56
  133. package/types/radix-ng-primitives-date-field.d.ts +49 -28
  134. package/types/radix-ng-primitives-dialog.d.ts +283 -165
  135. package/types/radix-ng-primitives-dismissable-layer.d.ts +15 -7
  136. package/types/radix-ng-primitives-drawer.d.ts +426 -0
  137. package/types/radix-ng-primitives-editable.d.ts +91 -14
  138. package/types/radix-ng-primitives-field.d.ts +374 -0
  139. package/types/radix-ng-primitives-fieldset.d.ts +49 -0
  140. package/types/radix-ng-primitives-focus-scope.d.ts +15 -6
  141. package/types/radix-ng-primitives-input.d.ts +87 -0
  142. package/types/radix-ng-primitives-label.d.ts +0 -1
  143. package/types/radix-ng-primitives-menu.d.ts +584 -99
  144. package/types/radix-ng-primitives-menubar.d.ts +61 -50
  145. package/types/radix-ng-primitives-meter.d.ts +194 -0
  146. package/types/radix-ng-primitives-navigation-menu.d.ts +422 -340
  147. package/types/radix-ng-primitives-number-field.d.ts +405 -145
  148. package/types/radix-ng-primitives-pagination.d.ts +2 -2
  149. package/types/radix-ng-primitives-popover.d.ts +366 -351
  150. package/types/radix-ng-primitives-popper.d.ts +68 -11
  151. package/types/radix-ng-primitives-portal.d.ts +14 -6
  152. package/types/radix-ng-primitives-presence.d.ts +28 -76
  153. package/types/radix-ng-primitives-preview-card.d.ts +359 -0
  154. package/types/radix-ng-primitives-progress.d.ts +175 -48
  155. package/types/radix-ng-primitives-radio.d.ts +55 -25
  156. package/types/radix-ng-primitives-roving-focus.d.ts +33 -23
  157. package/types/radix-ng-primitives-scroll-area.d.ts +253 -0
  158. package/types/radix-ng-primitives-select.d.ts +475 -177
  159. package/types/radix-ng-primitives-separator.d.ts +7 -32
  160. package/types/radix-ng-primitives-slider.d.ts +315 -201
  161. package/types/radix-ng-primitives-stepper.d.ts +5 -7
  162. package/types/radix-ng-primitives-switch.d.ts +86 -71
  163. package/types/radix-ng-primitives-tabs.d.ts +213 -79
  164. package/types/radix-ng-primitives-time-field.d.ts +42 -27
  165. package/types/radix-ng-primitives-toast.d.ts +378 -0
  166. package/types/radix-ng-primitives-toggle-group.d.ts +86 -164
  167. package/types/radix-ng-primitives-toggle.d.ts +43 -53
  168. package/types/radix-ng-primitives-toolbar.d.ts +164 -38
  169. package/types/radix-ng-primitives-tooltip.d.ts +348 -384
  170. package/types/radix-ng-primitives-visually-hidden.d.ts +19 -19
  171. package/dropdown-menu/README.md +0 -1
  172. package/fesm2022/radix-ng-primitives-dropdown-menu.mjs +0 -581
  173. package/fesm2022/radix-ng-primitives-dropdown-menu.mjs.map +0 -1
  174. package/fesm2022/radix-ng-primitives-hover-card.mjs +0 -1238
  175. package/fesm2022/radix-ng-primitives-hover-card.mjs.map +0 -1
  176. package/fesm2022/radix-ng-primitives-select2.mjs +0 -897
  177. package/fesm2022/radix-ng-primitives-select2.mjs.map +0 -1
  178. package/fesm2022/radix-ng-primitives-tooltip2.mjs +0 -735
  179. package/fesm2022/radix-ng-primitives-tooltip2.mjs.map +0 -1
  180. package/hover-card/README.md +0 -3
  181. package/select2/README.md +0 -3
  182. package/tooltip2/README.md +0 -3
  183. package/types/radix-ng-primitives-dropdown-menu.d.ts +0 -171
  184. package/types/radix-ng-primitives-hover-card.d.ts +0 -471
  185. package/types/radix-ng-primitives-select2.d.ts +0 -511
  186. package/types/radix-ng-primitives-tooltip2.d.ts +0 -325
@@ -1,25 +1,23 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, inject, Directive, input, computed, Component, ElementRef, numberAttribute, output, signal, afterNextRender, effect, NgModule } from '@angular/core';
3
- import { clamp, provideToken } from '@radix-ng/primitives/core';
2
+ import { Directive, input, Component, inject, ElementRef, Injector, PLATFORM_ID, numberAttribute, booleanAttribute, output, signal, computed, afterNextRender, effect, untracked, NgModule } from '@angular/core';
3
+ import { createContext, clamp, injectId, resizeEffect } from '@radix-ng/primitives/core';
4
+ import { isPlatformBrowser } from '@angular/common';
4
5
 
5
- const CROPPER_ROOT_CONTEXT = new InjectionToken('CROPPER_ROOT_CONTEXT');
6
- function injectCropperRootContext() {
7
- return inject(CROPPER_ROOT_CONTEXT);
8
- }
6
+ const [injectCropperRootContext, provideCropperRootContext] = createContext('CropperRoot');
9
7
 
10
8
  class RdxCropperCropAreaDirective {
11
9
  constructor() {
12
10
  this.rootContext = injectCropperRootContext();
13
11
  }
14
12
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxCropperCropAreaDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
15
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxCropperCropAreaDirective, isStandalone: true, selector: "[rdxCropperCropArea]", host: { properties: { "style": "rootContext.getCropAreaStyle()" } }, ngImport: i0 }); }
13
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxCropperCropAreaDirective, isStandalone: true, selector: "[rdxCropperCropArea]", host: { properties: { "style": "rootContext.cropAreaStyle()" } }, ngImport: i0 }); }
16
14
  }
17
15
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxCropperCropAreaDirective, decorators: [{
18
16
  type: Directive,
19
17
  args: [{
20
18
  selector: '[rdxCropperCropArea]',
21
19
  host: {
22
- '[style]': 'rootContext.getCropAreaStyle()'
20
+ '[style]': 'rootContext.cropAreaStyle()'
23
21
  }
24
22
  }]
25
23
  }] });
@@ -29,14 +27,14 @@ class RdxCropperDescriptionDirective {
29
27
  this.rootContext = injectCropperRootContext();
30
28
  }
31
29
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxCropperDescriptionDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
32
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxCropperDescriptionDirective, isStandalone: true, selector: "[rdxCropperDescription]", host: { properties: { "attr.id": "rootContext.descriptionId()" } }, ngImport: i0 }); }
30
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: RdxCropperDescriptionDirective, isStandalone: true, selector: "[rdxCropperDescription]", host: { properties: { "attr.id": "rootContext.descriptionId" } }, ngImport: i0 }); }
33
31
  }
34
32
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxCropperDescriptionDirective, decorators: [{
35
33
  type: Directive,
36
34
  args: [{
37
35
  selector: '[rdxCropperDescription]',
38
36
  host: {
39
- '[attr.id]': 'rootContext.descriptionId()'
37
+ '[attr.id]': 'rootContext.descriptionId'
40
38
  }
41
39
  }]
42
40
  }] });
@@ -46,18 +44,21 @@ class RdxCropperImageComponent {
46
44
  this.rootContext = injectCropperRootContext();
47
45
  this.imgClass = input(...(ngDevMode ? [undefined, { debugName: "imgClass" }] : /* istanbul ignore next */ []));
48
46
  this.imgStyles = input(...(ngDevMode ? [undefined, { debugName: "imgStyles" }] : /* istanbul ignore next */ []));
49
- this.imgClasses = computed(() => this.imgClass(), ...(ngDevMode ? [{ debugName: "imgClasses" }] : /* istanbul ignore next */ []));
50
- this.imgStyless = computed(() => this.imgStyles(), ...(ngDevMode ? [{ debugName: "imgStyless" }] : /* istanbul ignore next */ []));
47
+ /**
48
+ * `alt` text for the rendered image. Defaults to `''` (decorative screen readers skip it, since
49
+ * the cropper widget describes itself via the root's label/description). Set a non-empty value to
50
+ * give the image a meaningful accessible name.
51
+ */
52
+ this.imgAlt = input('', ...(ngDevMode ? [{ debugName: "imgAlt" }] : /* istanbul ignore next */ []));
51
53
  }
52
54
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxCropperImageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
53
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.9", type: RdxCropperImageComponent, isStandalone: true, selector: "[rdxCropperImage]", inputs: { imgClass: { classPropertyName: "imgClass", publicName: "imgClass", isSignal: true, isRequired: false, transformFunction: null }, imgStyles: { classPropertyName: "imgStyles", publicName: "imgStyles", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "style": "rootContext.getImageWrapperStyle()" } }, ngImport: i0, template: `
55
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.9", type: RdxCropperImageComponent, isStandalone: true, selector: "[rdxCropperImage]", inputs: { imgClass: { classPropertyName: "imgClass", publicName: "imgClass", isSignal: true, isRequired: false, transformFunction: null }, imgStyles: { classPropertyName: "imgStyles", publicName: "imgStyles", isSignal: true, isRequired: false, transformFunction: null }, imgAlt: { classPropertyName: "imgAlt", publicName: "imgAlt", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "style": "rootContext.imageWrapperStyle()" } }, ngImport: i0, template: `
54
56
  <img
55
- [class]="imgClasses()"
56
- [src]="rootContext.getImageProps()['src']"
57
- [alt]="rootContext.getImageProps()['alt']"
58
- [draggable]="rootContext.getImageProps()['draggable']"
59
- [style]="imgStyless()"
60
- aria-hidden="true"
57
+ [class]="imgClass()"
58
+ [src]="rootContext.image()"
59
+ [style]="imgStyles()"
60
+ [draggable]="false"
61
+ [alt]="imgAlt()"
61
62
  />
62
63
  `, isInline: true }); }
63
64
  }
@@ -66,24 +67,97 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
66
67
  args: [{
67
68
  selector: '[rdxCropperImage]',
68
69
  host: {
69
- '[style]': 'rootContext.getImageWrapperStyle()'
70
+ '[style]': 'rootContext.imageWrapperStyle()'
70
71
  },
71
72
  template: `
72
73
  <img
73
- [class]="imgClasses()"
74
- [src]="rootContext.getImageProps()['src']"
75
- [alt]="rootContext.getImageProps()['alt']"
76
- [draggable]="rootContext.getImageProps()['draggable']"
77
- [style]="imgStyless()"
78
- aria-hidden="true"
74
+ [class]="imgClass()"
75
+ [src]="rootContext.image()"
76
+ [style]="imgStyles()"
77
+ [draggable]="false"
78
+ [alt]="imgAlt()"
79
79
  />
80
80
  `
81
81
  }]
82
- }], propDecorators: { imgClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "imgClass", required: false }] }], imgStyles: [{ type: i0.Input, args: [{ isSignal: true, alias: "imgStyles", required: false }] }] } });
82
+ }], propDecorators: { imgClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "imgClass", required: false }] }], imgStyles: [{ type: i0.Input, args: [{ isSignal: true, alias: "imgStyles", required: false }] }], imgAlt: [{ type: i0.Input, args: [{ isSignal: true, alias: "imgAlt", required: false }] }] } });
83
+
84
+ /**
85
+ * Clamp a pan offset so the zoomed image always fully covers the crop window — the user can never
86
+ * drag an image edge inside the crop frame. The offset is measured from the centered position, so the
87
+ * allowed travel on each axis is half the overflow of the scaled wrapper past the crop window.
88
+ *
89
+ * Returns `{ x: 0, y: 0 }` when the geometry is not ready yet (any dimension `<= 0`).
90
+ */
91
+ function restrictOffset(offsetX, offsetY, zoom, geometry) {
92
+ const { wrapperWidth, wrapperHeight, cropWidth, cropHeight } = geometry;
93
+ if (wrapperWidth <= 0 || wrapperHeight <= 0 || cropWidth <= 0 || cropHeight <= 0) {
94
+ return { x: 0, y: 0 };
95
+ }
96
+ const maxDragX = Math.max(0, (wrapperWidth * zoom - cropWidth) / 2);
97
+ const maxDragY = Math.max(0, (wrapperHeight * zoom - cropHeight) / 2);
98
+ return {
99
+ x: clamp(offsetX, -maxDragX, maxDragX),
100
+ y: clamp(offsetY, -maxDragY, maxDragY)
101
+ };
102
+ }
103
+ /**
104
+ * Project the current pan/zoom into a crop rectangle in the source image's natural pixels.
105
+ *
106
+ * The wrapper is centered in the crop window and scaled by `zoom`; `baseScale` converts rendered px
107
+ * back to natural px. The result is rounded and clamped to the image bounds.
108
+ *
109
+ * Returns `null` when the geometry/image is not ready (any dimension `<= 0` or no image), or when the
110
+ * computed crop collapses to zero area.
111
+ */
112
+ function calculateCropData(offsetX, offsetY, zoom, geometry, image) {
113
+ const { wrapperWidth, wrapperHeight, cropWidth, cropHeight } = geometry;
114
+ const imgW = image.width;
115
+ const imgH = image.height;
116
+ if (!imgW || !imgH || wrapperWidth <= 0 || wrapperHeight <= 0 || cropWidth <= 0 || cropHeight <= 0) {
117
+ return null;
118
+ }
119
+ const scaledWrapperWidth = wrapperWidth * zoom;
120
+ const scaledWrapperHeight = wrapperHeight * zoom;
121
+ const topLeftOffsetX = offsetX + (cropWidth - scaledWrapperWidth) / 2;
122
+ const topLeftOffsetY = offsetY + (cropHeight - scaledWrapperHeight) / 2;
123
+ const baseScale = imgW / wrapperWidth;
124
+ if (isNaN(baseScale) || baseScale === 0) {
125
+ return null;
126
+ }
127
+ const sx = (-topLeftOffsetX * baseScale) / zoom;
128
+ const sy = (-topLeftOffsetY * baseScale) / zoom;
129
+ const sWidth = (cropWidth * baseScale) / zoom;
130
+ const sHeight = (cropHeight * baseScale) / zoom;
131
+ const finalX = clamp(Math.round(sx), 0, imgW);
132
+ const finalY = clamp(Math.round(sy), 0, imgH);
133
+ const finalWidth = clamp(Math.round(sWidth), 0, imgW - finalX);
134
+ const finalHeight = clamp(Math.round(sHeight), 0, imgH - finalY);
135
+ if (finalWidth <= 0 || finalHeight <= 0) {
136
+ return null;
137
+ }
138
+ return { x: finalX, y: finalY, width: finalWidth, height: finalHeight };
139
+ }
83
140
 
141
+ // Based on https://github.com/origin-space/image-cropper/blob/main/src/Cropper.tsx
142
+ /** Value equality for an `Area` so the crop `computed` only notifies when the rectangle changes. */
143
+ const areaEqual = (a, b) => a === b || (!!a && !!b && a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height);
144
+ /** Value equality for an `{x,y}` point so the clamped-offset `computed` only notifies on real moves. */
145
+ const pointEqual = (a, b) => a.x === b.x && a.y === b.y;
146
+ /** Exposes the root's public state to the child parts (image, crop-area, description). */
147
+ const rootContext = () => {
148
+ const instance = inject(RdxCropperRootDirective);
149
+ return {
150
+ image: instance.image,
151
+ imageWrapperStyle: instance.imageWrapperStyle,
152
+ cropAreaStyle: instance.cropAreaStyle,
153
+ descriptionId: instance.descriptionId
154
+ };
155
+ };
84
156
  class RdxCropperRootDirective {
85
157
  constructor() {
86
158
  this.elementRef = inject((ElementRef));
159
+ this.injector = inject(Injector);
160
+ this.isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
87
161
  this.CROPPER_DESC_WARN_MESSAGE = `Warning: \`Cropper\` requires a description element for accessibility.`;
88
162
  this.image = input.required(...(ngDevMode ? [{ debugName: "image" }] : /* istanbul ignore next */ []));
89
163
  this.cropPadding = input(25, { ...(ngDevMode ? { debugName: "cropPadding" } : /* istanbul ignore next */ {}), transform: numberAttribute });
@@ -91,49 +165,119 @@ class RdxCropperRootDirective {
91
165
  this.minZoom = input(1, { ...(ngDevMode ? { debugName: "minZoom" } : /* istanbul ignore next */ {}), transform: numberAttribute });
92
166
  this.maxZoom = input(3, { ...(ngDevMode ? { debugName: "maxZoom" } : /* istanbul ignore next */ {}), transform: numberAttribute });
93
167
  this.zoomSensitivity = input(0.005, { ...(ngDevMode ? { debugName: "zoomSensitivity" } : /* istanbul ignore next */ {}), transform: numberAttribute });
168
+ /** Pan distance (px) per arrow-key press. */
94
169
  this.keyboardStep = input(10, { ...(ngDevMode ? { debugName: "keyboardStep" } : /* istanbul ignore next */ {}), transform: numberAttribute });
170
+ /** Zoom delta per `+` / `-` / `PageUp` / `PageDown` press. */
171
+ this.zoomKeyboardStep = input(0.1, { ...(ngDevMode ? { debugName: "zoomKeyboardStep" } : /* istanbul ignore next */ {}), transform: numberAttribute });
95
172
  this.zoom = input(undefined, { ...(ngDevMode ? { debugName: "zoom" } : /* istanbul ignore next */ {}), transform: numberAttribute });
173
+ /** Accessible name for the cropper widget. */
174
+ this.ariaLabel = input('Interactive image cropper', ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
175
+ /** Disables all interaction (drag, wheel/pinch zoom, keyboard); exposed as `data-disabled`. */
176
+ this.disabled = input(false, { ...(ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
96
177
  this.onCropChange = output();
97
178
  this.onZoomChange = output();
98
179
  // State signals
99
180
  this.imgWidth = signal(null, ...(ngDevMode ? [{ debugName: "imgWidth" }] : /* istanbul ignore next */ []));
100
181
  this.imgHeight = signal(null, ...(ngDevMode ? [{ debugName: "imgHeight" }] : /* istanbul ignore next */ []));
101
- this.cropAreaWidth = signal(0, ...(ngDevMode ? [{ debugName: "cropAreaWidth" }] : /* istanbul ignore next */ []));
102
- this.cropAreaHeight = signal(0, ...(ngDevMode ? [{ debugName: "cropAreaHeight" }] : /* istanbul ignore next */ []));
182
+ /** Raw content-box size (px) of the root, fed by the ResizeObserver / initial measure. */
183
+ this.containerSize = signal({ width: 0, height: 0 }, ...(ngDevMode ? [{ debugName: "containerSize" }] : /* istanbul ignore next */ []));
184
+ /**
185
+ * Crop-area size derived from the container minus padding, fitted to `aspectRatio`. A `computed`
186
+ * (not a written signal) so it stays reactive to `aspectRatio` / `cropPadding` changes, not only
187
+ * to container resizes — previously these inputs were read inside the ResizeObserver closure and
188
+ * never recomputed until the next resize.
189
+ */
190
+ this.cropAreaSize = computed(() => {
191
+ const { width, height } = this.containerSize();
192
+ if (width <= 0 || height <= 0) {
193
+ return { width: 0, height: 0 };
194
+ }
195
+ const maxPossibleWidth = Math.max(0, width - this.cropPadding() * 2);
196
+ const maxPossibleHeight = Math.max(0, height - this.cropPadding() * 2);
197
+ const aspectRatio = this.aspectRatio();
198
+ if (maxPossibleWidth / aspectRatio >= maxPossibleHeight) {
199
+ return { width: maxPossibleHeight * aspectRatio, height: maxPossibleHeight };
200
+ }
201
+ return { width: maxPossibleWidth, height: maxPossibleWidth / aspectRatio };
202
+ }, ...(ngDevMode ? [{ debugName: "cropAreaSize" }] : /* istanbul ignore next */ []));
203
+ this.cropAreaWidth = computed(() => this.cropAreaSize().width, ...(ngDevMode ? [{ debugName: "cropAreaWidth" }] : /* istanbul ignore next */ []));
204
+ this.cropAreaHeight = computed(() => this.cropAreaSize().height, ...(ngDevMode ? [{ debugName: "cropAreaHeight" }] : /* istanbul ignore next */ []));
103
205
  this.imageWrapperWidth = signal(0, ...(ngDevMode ? [{ debugName: "imageWrapperWidth" }] : /* istanbul ignore next */ []));
104
206
  this.imageWrapperHeight = signal(0, ...(ngDevMode ? [{ debugName: "imageWrapperHeight" }] : /* istanbul ignore next */ []));
105
- this.offsetX = signal(0, ...(ngDevMode ? [{ debugName: "offsetX" }] : /* istanbul ignore next */ []));
106
- this.offsetY = signal(0, ...(ngDevMode ? [{ debugName: "offsetY" }] : /* istanbul ignore next */ []));
207
+ /** Raw (unclamped) pan-offset intent (px) written by gestures; clamping lives in `clampedOffset`. */
208
+ this.offset = signal({ x: 0, y: 0 }, ...(ngDevMode ? [{ debugName: "offset" }] : /* istanbul ignore next */ []));
107
209
  this.internalZoom = signal(this.minZoom(), ...(ngDevMode ? [{ debugName: "internalZoom" }] : /* istanbul ignore next */ []));
108
210
  this.isDragging = signal(false, ...(ngDevMode ? [{ debugName: "isDragging" }] : /* istanbul ignore next */ []));
109
- this.descriptionId = signal(`cropper-${Math.random().toString(36).substring(2, 9)}`, ...(ngDevMode ? [{ debugName: "descriptionId" }] : /* istanbul ignore next */ []));
211
+ // SSR-stable, deterministic id (the project's CDK-free `_IdGenerator` replacement) so the
212
+ // `aria-describedby` reference matches between server and client renders.
213
+ this.descriptionId = injectId('rdx-cropper-description-');
110
214
  this.isZoomControlled = computed(() => this.zoom() !== undefined, ...(ngDevMode ? [{ debugName: "isZoomControlled" }] : /* istanbul ignore next */ []));
111
215
  this.effectiveZoom = computed(() => (this.isZoomControlled() ? this.zoom() : this.internalZoom()), ...(ngDevMode ? [{ debugName: "effectiveZoom" }] : /* istanbul ignore next */ []));
112
- this.zoomValueText = computed(() => {
113
- const zoomPercent = this.effectiveZoom() * 100;
114
- return `Zoom: ${zoomPercent.toFixed(0)}%`;
115
- }, ...(ngDevMode ? [{ debugName: "zoomValueText" }] : /* istanbul ignore next */ []));
216
+ /**
217
+ * The applied pan offset (px): the raw intent clamped to keep the image covering the crop window
218
+ * at the current geometry/zoom. Derived (not an effect) so it self-corrects when the container
219
+ * resizes or the zoom/aspect-ratio changes no write-back, no `untracked` re-entrancy. This is the
220
+ * value the view renders and the crop math reads.
221
+ */
222
+ this.clampedOffset = computed(() => this.restrictOffset(this.offset().x, this.offset().y, this.effectiveZoom()), { ...(ngDevMode ? { debugName: "clampedOffset" } : /* istanbul ignore next */ {}), equal: pointEqual });
223
+ /**
224
+ * Crop rectangle derived from the rendered pan/zoom — the single source of truth for emission.
225
+ * `onCropChange` fires from one effect watching this, so interactions/handlers never emit directly
226
+ * (which previously double-emitted). Value equality keeps it from notifying on equal results.
227
+ */
228
+ this.cropData = computed(() => calculateCropData(this.clampedOffset().x, this.clampedOffset().y, this.effectiveZoom(), this.geometry(), {
229
+ width: this.imgWidth(),
230
+ height: this.imgHeight()
231
+ }), { ...(ngDevMode ? { debugName: "cropData" } : /* istanbul ignore next */ {}), equal: areaEqual });
116
232
  // Refs
117
233
  this.dragStartPoint = signal({ x: 0, y: 0 }, ...(ngDevMode ? [{ debugName: "dragStartPoint" }] : /* istanbul ignore next */ []));
118
234
  this.dragStartOffset = signal({ x: 0, y: 0 }, ...(ngDevMode ? [{ debugName: "dragStartOffset" }] : /* istanbul ignore next */ []));
119
- this.latestRestrictedOffset = signal({ x: 0, y: 0 }, ...(ngDevMode ? [{ debugName: "latestRestrictedOffset" }] : /* istanbul ignore next */ []));
120
235
  this.latestZoom = signal(this.minZoom(), ...(ngDevMode ? [{ debugName: "latestZoom" }] : /* istanbul ignore next */ []));
121
- this.isInitialSetupDone = signal(false, ...(ngDevMode ? [{ debugName: "isInitialSetupDone" }] : /* istanbul ignore next */ []));
122
236
  this.initialPinchDistance = signal(0, ...(ngDevMode ? [{ debugName: "initialPinchDistance" }] : /* istanbul ignore next */ []));
123
237
  this.initialPinchZoom = signal(1, ...(ngDevMode ? [{ debugName: "initialPinchZoom" }] : /* istanbul ignore next */ []));
124
238
  this.isPinching = signal(false, ...(ngDevMode ? [{ debugName: "isPinching" }] : /* istanbul ignore next */ []));
125
239
  this.hasWarned = signal(false, ...(ngDevMode ? [{ debugName: "hasWarned" }] : /* istanbul ignore next */ []));
240
+ /**
241
+ * Inline style for the image wrapper: measured size, centered in the root, then translated and
242
+ * scaled by the current pan offset and zoom. A `computed` (not a method) so the `[style]` binding
243
+ * only re-applies when an input actually changes — a per-change-detection method call would
244
+ * allocate a new object every tick and force a constant re-bind.
245
+ */
246
+ this.imageWrapperStyle = computed(() => {
247
+ const wrapperW = this.imageWrapperWidth();
248
+ const wrapperH = this.imageWrapperHeight();
249
+ const { x: offsetX, y: offsetY } = this.clampedOffset();
250
+ const zoom = this.effectiveZoom();
251
+ return {
252
+ width: `${wrapperW}px`,
253
+ height: `${wrapperH}px`,
254
+ transform: `translate3d(${offsetX}px, ${offsetY}px, 0px) scale(${zoom})`,
255
+ position: 'absolute',
256
+ left: `calc(50% - ${wrapperW / 2}px)`,
257
+ top: `calc(50% - ${wrapperH / 2}px)`,
258
+ willChange: 'transform'
259
+ };
260
+ }, ...(ngDevMode ? [{ debugName: "imageWrapperStyle" }] : /* istanbul ignore next */ []));
261
+ /** Inline style for the crop-area overlay (its measured width/height). */
262
+ this.cropAreaStyle = computed(() => ({
263
+ width: `${this.cropAreaWidth()}px`,
264
+ height: `${this.cropAreaHeight()}px`
265
+ }), ...(ngDevMode ? [{ debugName: "cropAreaStyle" }] : /* istanbul ignore next */ []));
126
266
  afterNextRender(() => {
127
267
  this.initializeContainerDimensions();
128
268
  });
129
269
  this.setupImageLoadEffect();
130
270
  this.setupDimensionsEffects();
131
- this.setupCropCalculationEffect();
132
271
  this.setupAccessibilityWarningEffect();
133
272
  this.setupEventListenersEffect();
134
273
  effect(() => {
135
274
  this.latestZoom.set(this.effectiveZoom());
136
275
  });
276
+ // Single source of crop emission: emit whenever the derived crop rectangle changes.
277
+ effect(() => {
278
+ const data = this.cropData();
279
+ untracked(() => this.onCropChange.emit(data));
280
+ });
137
281
  }
138
282
  updateZoom(newZoomValue) {
139
283
  const clampedZoom = clamp(newZoomValue, this.minZoom(), this.maxZoom());
@@ -146,20 +290,18 @@ class RdxCropperRootDirective {
146
290
  initializeContainerDimensions() {
147
291
  const element = this.elementRef.nativeElement;
148
292
  if (element && element.clientWidth > 0 && element.clientHeight > 0) {
149
- this.cropAreaWidth.set(Math.max(0, element.clientWidth - this.cropPadding() * 2));
150
- this.cropAreaHeight.set(Math.max(0, element.clientHeight - this.cropPadding() * 2));
293
+ // Seed the size for the first paint; the ResizeObserver keeps it in sync afterwards.
294
+ this.containerSize.set({ width: element.clientWidth, height: element.clientHeight });
151
295
  }
152
296
  }
153
297
  setupImageLoadEffect() {
154
298
  effect(() => {
155
299
  const image = this.image();
156
- this.offsetX.set(0);
157
- this.offsetY.set(0);
300
+ this.offset.set({ x: 0, y: 0 });
158
301
  if (!this.isZoomControlled()) {
159
302
  this.internalZoom.set(this.minZoom());
160
303
  }
161
- this.isInitialSetupDone.set(false);
162
- if (!image) {
304
+ if (!image || !this.isBrowser) {
163
305
  this.imgWidth.set(null);
164
306
  this.imgHeight.set(null);
165
307
  return;
@@ -185,44 +327,20 @@ class RdxCropperRootDirective {
185
327
  });
186
328
  }
187
329
  setupDimensionsEffects() {
188
- effect(() => {
189
- const element = this.elementRef.nativeElement;
190
- if (!element)
191
- return;
192
- const updateDimensions = (width, height) => {
193
- if (width <= 0 || height <= 0) {
194
- this.cropAreaWidth.set(0);
195
- this.cropAreaHeight.set(0);
196
- return;
197
- }
198
- const maxPossibleWidth = Math.max(0, width - this.cropPadding() * 2);
199
- const maxPossibleHeight = Math.max(0, height - this.cropPadding() * 2);
200
- let targetCropW, targetCropH;
201
- if (maxPossibleWidth / this.aspectRatio() >= maxPossibleHeight) {
202
- targetCropH = maxPossibleHeight;
203
- targetCropW = targetCropH * this.aspectRatio();
204
- }
205
- else {
206
- targetCropW = maxPossibleWidth;
207
- targetCropH = targetCropW / this.aspectRatio();
208
- }
209
- this.cropAreaWidth.set(targetCropW);
210
- this.cropAreaHeight.set(targetCropH);
211
- };
212
- const observer = new ResizeObserver((entries) => {
330
+ // Track the container's content-box size via the shared resize-observer effect; `cropAreaSize`
331
+ // derives crop dimensions from it reactively (so changing `aspectRatio` / `cropPadding`
332
+ // recomputes without waiting for a resize). `element` is null on the server, so no observer is
333
+ // constructed (SSR-safe).
334
+ resizeEffect({
335
+ injector: this.injector,
336
+ element: computed(() => (this.isBrowser ? this.elementRef.nativeElement : null)),
337
+ onResize: (entries) => {
213
338
  for (const entry of entries) {
214
339
  const { width, height } = entry.contentRect;
215
340
  if (width > 0 && height > 0)
216
- updateDimensions(width, height);
341
+ this.containerSize.set({ width, height });
217
342
  }
218
- });
219
- observer.observe(element);
220
- const initialWidth = element.clientWidth;
221
- const initialHeight = element.clientHeight;
222
- if (initialWidth > 0 && initialHeight > 0) {
223
- updateDimensions(initialWidth, initialHeight);
224
343
  }
225
- return () => observer.disconnect();
226
344
  });
227
345
  // Update image wrapper dimensions when crop area or image dimensions change
228
346
  effect(() => {
@@ -250,110 +368,25 @@ class RdxCropperRootDirective {
250
368
  this.imageWrapperHeight.set(targetWrapperHeight);
251
369
  });
252
370
  }
253
- restrictOffset(dragOffsetX, dragOffsetY, currentZoom) {
254
- const wrapperW = this.imageWrapperWidth();
255
- const wrapperH = this.imageWrapperHeight();
256
- const cropW = this.cropAreaWidth();
257
- const cropH = this.cropAreaHeight();
258
- if (wrapperW <= 0 || wrapperH <= 0 || cropW <= 0 || cropH <= 0) {
259
- return { x: 0, y: 0 };
260
- }
261
- const effectiveWrapperWidth = wrapperW * currentZoom;
262
- const effectiveWrapperHeight = wrapperH * currentZoom;
263
- const maxDragX = Math.max(0, (effectiveWrapperWidth - cropW) / 2);
264
- const maxDragY = Math.max(0, (effectiveWrapperHeight - cropH) / 2);
371
+ /** Current rendered geometry the crop math operates on, read from the state signals. */
372
+ geometry() {
265
373
  return {
266
- x: clamp(dragOffsetX, -maxDragX, maxDragX),
267
- y: clamp(dragOffsetY, -maxDragY, maxDragY)
374
+ wrapperWidth: this.imageWrapperWidth(),
375
+ wrapperHeight: this.imageWrapperHeight(),
376
+ cropWidth: this.cropAreaWidth(),
377
+ cropHeight: this.cropAreaHeight()
268
378
  };
269
379
  }
270
- calculateCropData(finalOffsetX, finalOffsetY, finalZoom) {
271
- const currentOffsetX = finalOffsetX ?? this.latestRestrictedOffset().x;
272
- const currentOffsetY = finalOffsetY ?? this.latestRestrictedOffset().y;
273
- const currentZoom = finalZoom ?? this.effectiveZoom();
274
- const imgW = this.imgWidth();
275
- const imgH = this.imgHeight();
276
- const wrapperW = this.imageWrapperWidth();
277
- const wrapperH = this.imageWrapperHeight();
278
- const cropW = this.cropAreaWidth();
279
- const cropH = this.cropAreaHeight();
280
- if (!imgW || !imgH || wrapperW <= 0 || wrapperH <= 0 || cropW <= 0 || cropH <= 0) {
281
- return null;
282
- }
283
- const scaledWrapperWidth = wrapperW * currentZoom;
284
- const scaledWrapperHeight = wrapperH * currentZoom;
285
- const topLeftOffsetX = currentOffsetX + (cropW - scaledWrapperWidth) / 2;
286
- const topLeftOffsetY = currentOffsetY + (cropH - scaledWrapperHeight) / 2;
287
- const baseScale = imgW / wrapperW;
288
- if (isNaN(baseScale) || baseScale === 0) {
289
- return null;
290
- }
291
- const sx = (-topLeftOffsetX * baseScale) / currentZoom;
292
- const sy = (-topLeftOffsetY * baseScale) / currentZoom;
293
- const sWidth = (cropW * baseScale) / currentZoom;
294
- const sHeight = (cropH * baseScale) / currentZoom;
295
- const finalX = clamp(Math.round(sx), 0, imgW);
296
- const finalY = clamp(Math.round(sy), 0, imgH);
297
- const finalWidth = clamp(Math.round(sWidth), 0, imgW - finalX);
298
- const finalHeight = clamp(Math.round(sHeight), 0, imgH - finalY);
299
- if (finalWidth <= 0 || finalHeight <= 0) {
300
- return null;
301
- }
302
- return { x: finalX, y: finalY, width: finalWidth, height: finalHeight };
303
- }
304
- setupCropCalculationEffect() {
305
- effect(() => {
306
- const wrapperW = this.imageWrapperWidth();
307
- const wrapperH = this.imageWrapperHeight();
308
- const cropW = this.cropAreaWidth();
309
- const cropH = this.cropAreaHeight();
310
- const currentZoom = this.effectiveZoom();
311
- if (wrapperW > 0 && wrapperH > 0 && cropW > 0 && cropH > 0) {
312
- if (!this.isInitialSetupDone()) {
313
- const restrictedInitial = this.restrictOffset(0, 0, currentZoom);
314
- this.offsetX.set(restrictedInitial.x);
315
- this.offsetY.set(restrictedInitial.y);
316
- if (!this.isZoomControlled()) {
317
- this.internalZoom.set(currentZoom);
318
- }
319
- this.dragStartOffset.set(restrictedInitial);
320
- this.latestRestrictedOffset.set(restrictedInitial);
321
- this.latestZoom.set(currentZoom);
322
- this.onCropChange.emit(this.calculateCropData(restrictedInitial.x, restrictedInitial.y, currentZoom));
323
- this.isInitialSetupDone.set(true);
324
- }
325
- else {
326
- const currentX = this.latestRestrictedOffset().x;
327
- const currentY = this.latestRestrictedOffset().y;
328
- const restrictedCurrent = this.restrictOffset(currentX, currentY, currentZoom);
329
- if (restrictedCurrent.x !== currentX || restrictedCurrent.y !== currentY) {
330
- this.offsetX.set(restrictedCurrent.x);
331
- this.offsetY.set(restrictedCurrent.y);
332
- this.latestRestrictedOffset.set(restrictedCurrent);
333
- this.dragStartOffset.set(restrictedCurrent);
334
- }
335
- this.onCropChange.emit(this.calculateCropData(restrictedCurrent.x, restrictedCurrent.y, currentZoom));
336
- }
337
- }
338
- else {
339
- this.isInitialSetupDone.set(false);
340
- this.offsetX.set(0);
341
- this.offsetY.set(0);
342
- if (!this.isZoomControlled()) {
343
- this.internalZoom.set(this.minZoom());
344
- }
345
- this.dragStartOffset.set({ x: 0, y: 0 });
346
- this.latestRestrictedOffset.set({ x: 0, y: 0 });
347
- this.latestZoom.set(currentZoom);
348
- this.onCropChange.emit(null);
349
- }
350
- });
380
+ restrictOffset(dragOffsetX, dragOffsetY, currentZoom) {
381
+ return restrictOffset(dragOffsetX, dragOffsetY, currentZoom, this.geometry());
351
382
  }
352
383
  setupAccessibilityWarningEffect() {
384
+ if (!this.isBrowser)
385
+ return;
353
386
  effect(() => {
354
387
  const checkTimeout = setTimeout(() => {
355
388
  if (this.elementRef.nativeElement && !this.hasWarned()) {
356
- const hasDescription = document.getElementById(this.descriptionId());
389
+ const hasDescription = document.getElementById(this.descriptionId);
357
390
  if (!hasDescription) {
358
391
  console.warn(this.CROPPER_DESC_WARN_MESSAGE);
359
392
  this.hasWarned.set(true);
@@ -363,22 +396,36 @@ class RdxCropperRootDirective {
363
396
  return () => clearTimeout(checkTimeout);
364
397
  });
365
398
  }
399
+ /**
400
+ * Single attachment point for every interaction listener. Re-runs on `disabled()`, so a disabled
401
+ * cropper has NO interaction listeners bound at all — there is no per-handler `disabled` check to
402
+ * forget, and a new gesture path can't accidentally bypass the gate. Uses `{ passive: false }` so
403
+ * the handlers can `preventDefault()` (wheel/touch scrolling, arrow-key page scroll).
404
+ */
366
405
  setupEventListenersEffect() {
406
+ if (!this.isBrowser)
407
+ return;
367
408
  effect((onCleanup) => {
368
409
  const node = this.elementRef.nativeElement;
369
- if (!node)
410
+ if (!node || this.disabled())
370
411
  return;
371
412
  const options = { passive: false };
413
+ const mouseDownHandler = (e) => this.onMouseDown(e);
414
+ const keyDownHandler = (e) => this.onKeyDown(e);
372
415
  const wheelHandler = (e) => this.handleWheel(e);
373
416
  const touchStartHandler = (e) => this.handleTouchStart(e);
374
417
  const touchMoveHandler = (e) => this.handleTouchMove(e);
375
418
  const touchEndHandler = (e) => this.handleTouchEnd(e);
419
+ node.addEventListener('mousedown', mouseDownHandler, options);
420
+ node.addEventListener('keydown', keyDownHandler, options);
376
421
  node.addEventListener('wheel', wheelHandler, options);
377
422
  node.addEventListener('touchstart', touchStartHandler, options);
378
423
  node.addEventListener('touchmove', touchMoveHandler, options);
379
424
  node.addEventListener('touchend', touchEndHandler, options);
380
425
  node.addEventListener('touchcancel', touchEndHandler, options);
381
426
  onCleanup(() => {
427
+ node.removeEventListener('mousedown', mouseDownHandler, options);
428
+ node.removeEventListener('keydown', keyDownHandler, options);
382
429
  node.removeEventListener('wheel', wheelHandler, options);
383
430
  node.removeEventListener('touchstart', touchStartHandler, options);
384
431
  node.removeEventListener('touchmove', touchMoveHandler, options);
@@ -387,13 +434,6 @@ class RdxCropperRootDirective {
387
434
  });
388
435
  });
389
436
  }
390
- handleInteractionEnd() {
391
- const finalData = this.calculateCropData(this.latestRestrictedOffset().x, this.latestRestrictedOffset().y, this.effectiveZoom());
392
- this.onCropChange.emit(finalData);
393
- }
394
- /**
395
- * @ignore
396
- */
397
437
  onMouseDown(e) {
398
438
  if (e.button !== 0)
399
439
  return;
@@ -401,52 +441,49 @@ class RdxCropperRootDirective {
401
441
  this.isDragging.set(true);
402
442
  this.isPinching.set(false);
403
443
  this.dragStartPoint.set({ x: e.clientX, y: e.clientY });
404
- this.dragStartOffset.set(this.latestRestrictedOffset());
444
+ this.dragStartOffset.set(this.clampedOffset());
405
445
  const handleMouseMove = (ev) => {
406
446
  const deltaX = ev.clientX - this.dragStartPoint().x;
407
447
  const deltaY = ev.clientY - this.dragStartPoint().y;
408
- const targetOffsetX = this.dragStartOffset().x + deltaX;
409
- const targetOffsetY = this.dragStartOffset().y + deltaY;
410
- const restricted = this.restrictOffset(targetOffsetX, targetOffsetY, this.effectiveZoom());
411
- this.latestRestrictedOffset.set(restricted);
412
- this.offsetX.set(restricted.x);
413
- this.offsetY.set(restricted.y);
448
+ this.offset.set({ x: this.dragStartOffset().x + deltaX, y: this.dragStartOffset().y + deltaY });
414
449
  };
415
450
  const handleMouseUp = () => {
416
451
  this.isDragging.set(false);
417
452
  window.removeEventListener('mousemove', handleMouseMove);
418
453
  window.removeEventListener('mouseup', handleMouseUp);
419
- this.handleInteractionEnd();
420
454
  };
421
455
  window.addEventListener('mousemove', handleMouseMove);
422
456
  window.addEventListener('mouseup', handleMouseUp);
423
457
  }
458
+ /**
459
+ * Zoom toward an anchor point (coordinates relative to the root's center). `fromZoom`/`fromOffset`
460
+ * are the zoom/offset the anchor is measured against — the live state for wheel/keyboard, the
461
+ * gesture-start baseline for pinch. Emits the zoom request via {@link updateZoom}, then re-anchors
462
+ * the pan offset **only when the zoom is uncontrolled**: in controlled mode the rendered zoom does
463
+ * not change until the parent writes `zoom` back, so writing an offset for a not-yet-applied zoom
464
+ * would pan the image without rescaling it (`clampedOffset` re-derives once the new zoom applies).
465
+ */
466
+ zoomToPoint(pointerX, pointerY, targetZoom, fromZoom, fromOffset) {
467
+ const clampedZoom = clamp(targetZoom, this.minZoom(), this.maxZoom());
468
+ if (clampedZoom === this.effectiveZoom())
469
+ return;
470
+ const imagePointX = (pointerX - fromOffset.x) / fromZoom;
471
+ const imagePointY = (pointerY - fromOffset.y) / fromZoom;
472
+ const finalNewZoom = this.updateZoom(clampedZoom);
473
+ if (this.isZoomControlled())
474
+ return;
475
+ this.offset.set({ x: pointerX - imagePointX * finalNewZoom, y: pointerY - imagePointY * finalNewZoom });
476
+ }
424
477
  handleWheel(e) {
425
478
  e.preventDefault();
426
479
  e.stopPropagation();
427
480
  if (!this.elementRef.nativeElement || this.imageWrapperWidth() <= 0 || this.imageWrapperHeight() <= 0)
428
481
  return;
429
- const currentZoom = this.latestZoom();
430
- const currentOffsetX = this.latestRestrictedOffset().x;
431
- const currentOffsetY = this.latestRestrictedOffset().y;
432
482
  const delta = e.deltaY * -this.zoomSensitivity();
433
- const targetZoom = currentZoom + delta;
434
- if (clamp(targetZoom, this.minZoom(), this.maxZoom()) === currentZoom)
435
- return;
436
483
  const rect = this.elementRef.nativeElement.getBoundingClientRect();
437
484
  const pointerX = e.clientX - rect.left - rect.width / 2;
438
485
  const pointerY = e.clientY - rect.top - rect.height / 2;
439
- const imagePointX = (pointerX - currentOffsetX) / currentZoom;
440
- const imagePointY = (pointerY - currentOffsetY) / currentZoom;
441
- const finalNewZoom = this.updateZoom(targetZoom);
442
- const newOffsetX = pointerX - imagePointX * finalNewZoom;
443
- const newOffsetY = pointerY - imagePointY * finalNewZoom;
444
- const restrictedNewOffset = this.restrictOffset(newOffsetX, newOffsetY, finalNewZoom);
445
- this.offsetX.set(restrictedNewOffset.x);
446
- this.offsetY.set(restrictedNewOffset.y);
447
- this.latestRestrictedOffset.set(restrictedNewOffset);
448
- const finalData = this.calculateCropData(restrictedNewOffset.x, restrictedNewOffset.y, finalNewZoom);
449
- this.onCropChange.emit(finalData);
486
+ this.zoomToPoint(pointerX, pointerY, this.effectiveZoom() + delta, this.effectiveZoom(), this.clampedOffset());
450
487
  }
451
488
  getPinchDistance(touches) {
452
489
  return Math.sqrt(Math.pow(touches[1].clientX - touches[0].clientX, 2) + Math.pow(touches[1].clientY - touches[0].clientY, 2));
@@ -466,14 +503,14 @@ class RdxCropperRootDirective {
466
503
  this.isDragging.set(true);
467
504
  this.isPinching.set(false);
468
505
  this.dragStartPoint.set({ x: touches[0].clientX, y: touches[0].clientY });
469
- this.dragStartOffset.set(this.latestRestrictedOffset());
506
+ this.dragStartOffset.set(this.clampedOffset());
470
507
  }
471
508
  else if (touches.length === 2) {
472
509
  this.isDragging.set(false);
473
510
  this.isPinching.set(true);
474
511
  this.initialPinchDistance.set(this.getPinchDistance(touches));
475
512
  this.initialPinchZoom.set(this.latestZoom());
476
- this.dragStartOffset.set(this.latestRestrictedOffset());
513
+ this.dragStartOffset.set(this.clampedOffset());
477
514
  }
478
515
  }
479
516
  handleTouchMove(e) {
@@ -484,37 +521,16 @@ class RdxCropperRootDirective {
484
521
  if (touches.length === 1 && this.isDragging() && !this.isPinching()) {
485
522
  const deltaX = touches[0].clientX - this.dragStartPoint().x;
486
523
  const deltaY = touches[0].clientY - this.dragStartPoint().y;
487
- const targetOffsetX = this.dragStartOffset().x + deltaX;
488
- const targetOffsetY = this.dragStartOffset().y + deltaY;
489
- const restricted = this.restrictOffset(targetOffsetX, targetOffsetY, this.effectiveZoom());
490
- this.latestRestrictedOffset.set(restricted);
491
- this.offsetX.set(restricted.x);
492
- this.offsetY.set(restricted.y);
524
+ this.offset.set({ x: this.dragStartOffset().x + deltaX, y: this.dragStartOffset().y + deltaY });
493
525
  }
494
526
  else if (touches.length === 2 && this.isPinching()) {
495
- const currentPinchDistance = this.getPinchDistance(touches);
496
- const scale = currentPinchDistance / this.initialPinchDistance();
497
- const currentZoom = this.initialPinchZoom();
498
- const targetZoom = currentZoom * scale;
499
- if (clamp(targetZoom, this.minZoom(), this.maxZoom()) === this.latestZoom())
500
- return;
527
+ const scale = this.getPinchDistance(touches) / this.initialPinchDistance();
501
528
  const pinchCenter = this.getPinchCenter(touches);
502
529
  const rect = this.elementRef.nativeElement.getBoundingClientRect();
503
530
  const pinchCenterX = pinchCenter.x - rect.left - rect.width / 2;
504
531
  const pinchCenterY = pinchCenter.y - rect.top - rect.height / 2;
505
- const currentOffsetX = this.dragStartOffset().x;
506
- const currentOffsetY = this.dragStartOffset().y;
507
- const imagePointX = (pinchCenterX - currentOffsetX) / currentZoom;
508
- const imagePointY = (pinchCenterY - currentOffsetY) / currentZoom;
509
- const finalNewZoom = this.updateZoom(targetZoom);
510
- const newOffsetX = pinchCenterX - imagePointX * finalNewZoom;
511
- const newOffsetY = pinchCenterY - imagePointY * finalNewZoom;
512
- const restrictedNewOffset = this.restrictOffset(newOffsetX, newOffsetY, finalNewZoom);
513
- this.offsetX.set(restrictedNewOffset.x);
514
- this.offsetY.set(restrictedNewOffset.y);
515
- this.latestRestrictedOffset.set(restrictedNewOffset);
516
- const finalData = this.calculateCropData(restrictedNewOffset.x, restrictedNewOffset.y, finalNewZoom);
517
- this.onCropChange.emit(finalData);
532
+ // Pinch is baseline-relative: anchor against the zoom/offset captured at gesture start.
533
+ this.zoomToPoint(pinchCenterX, pinchCenterY, this.initialPinchZoom() * scale, this.initialPinchZoom(), this.dragStartOffset());
518
534
  }
519
535
  }
520
536
  handleTouchEnd(e) {
@@ -525,26 +541,22 @@ class RdxCropperRootDirective {
525
541
  if (touches.length === 1) {
526
542
  this.isDragging.set(true);
527
543
  this.dragStartPoint.set({ x: touches[0].clientX, y: touches[0].clientY });
528
- this.dragStartOffset.set(this.latestRestrictedOffset());
544
+ this.dragStartOffset.set(this.clampedOffset());
529
545
  }
530
546
  else {
531
547
  this.isDragging.set(false);
532
- this.handleInteractionEnd();
533
548
  }
534
549
  }
535
550
  else if (this.isDragging() && touches.length === 0) {
536
551
  this.isDragging.set(false);
537
- this.handleInteractionEnd();
538
552
  }
539
553
  }
540
- /**
541
- * @ignore
542
- */
543
554
  onKeyDown(e) {
544
555
  if (this.imageWrapperWidth() <= 0)
545
556
  return;
546
- let targetOffsetX = this.latestRestrictedOffset().x;
547
- let targetOffsetY = this.latestRestrictedOffset().y;
557
+ const base = this.clampedOffset();
558
+ let targetOffsetX = base.x;
559
+ let targetOffsetY = base.y;
548
560
  // eslint-disable-next-line no-useless-assignment
549
561
  let moved = false;
550
562
  switch (e.key) {
@@ -564,89 +576,57 @@ class RdxCropperRootDirective {
564
576
  targetOffsetX -= this.keyboardStep();
565
577
  moved = true;
566
578
  break;
579
+ case '+':
580
+ case '=':
581
+ case 'PageUp':
582
+ e.preventDefault();
583
+ this.zoomFromCenter(this.zoomKeyboardStep());
584
+ return;
585
+ case '-':
586
+ case '_':
587
+ case 'PageDown':
588
+ e.preventDefault();
589
+ this.zoomFromCenter(-this.zoomKeyboardStep());
590
+ return;
567
591
  default:
568
592
  return;
569
593
  }
570
594
  if (moved) {
571
595
  e.preventDefault();
572
- const restricted = this.restrictOffset(targetOffsetX, targetOffsetY, this.effectiveZoom());
573
- if (restricted.x !== this.latestRestrictedOffset().x || restricted.y !== this.latestRestrictedOffset().y) {
574
- this.latestRestrictedOffset.set(restricted);
575
- this.offsetX.set(restricted.x);
576
- this.offsetY.set(restricted.y);
577
- }
596
+ // Write the raw target; `clampedOffset` clamps it (and dedups a no-op at the boundary).
597
+ this.offset.set({ x: targetOffsetX, y: targetOffsetY });
578
598
  }
579
599
  }
580
- /**
581
- * @ignore
582
- */
583
- onKeyUp(e) {
584
- if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
585
- this.handleInteractionEnd();
586
- }
587
- }
588
- /**
589
- * @ignore
590
- */
591
- getImageProps() {
592
- return {
593
- src: this.image(),
594
- alt: 'Image being cropped',
595
- draggable: false,
596
- 'aria-hidden': true
597
- };
598
- }
599
- /**
600
- * @ignore
601
- */
602
- getImageWrapperStyle() {
603
- const wrapperW = this.imageWrapperWidth();
604
- const wrapperH = this.imageWrapperHeight();
605
- const offsetX = this.offsetX();
606
- const offsetY = this.offsetY();
607
- const zoom = this.effectiveZoom();
608
- return {
609
- width: `${wrapperW}px`,
610
- height: `${wrapperH}px`,
611
- transform: `translate3d(${offsetX}px, ${offsetY}px, 0px) scale(${zoom})`,
612
- position: 'absolute',
613
- left: `calc(50% - ${wrapperW / 2}px)`,
614
- top: `calc(50% - ${wrapperH / 2}px)`,
615
- willChange: 'transform'
616
- };
617
- }
618
- /**
619
- * @ignore
620
- */
621
- getCropAreaStyle() {
622
- return {
623
- width: `${this.cropAreaWidth()}px`,
624
- height: `${this.cropAreaHeight()}px`
625
- };
600
+ /** Zoom by `delta` keeping the crop center fixed (the keyboard counterpart of wheel/pinch zoom). */
601
+ zoomFromCenter(delta) {
602
+ if (this.imageWrapperWidth() <= 0 || this.imageWrapperHeight() <= 0)
603
+ return;
604
+ // Anchor at the crop center (pointer 0,0 relative to center) against the live zoom/offset.
605
+ this.zoomToPoint(0, 0, this.effectiveZoom() + delta, this.effectiveZoom(), this.clampedOffset());
626
606
  }
627
607
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxCropperRootDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
628
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxCropperRootDirective, isStandalone: true, selector: "[rdxCropperRoot]", inputs: { image: { classPropertyName: "image", publicName: "image", isSignal: true, isRequired: true, transformFunction: null }, cropPadding: { classPropertyName: "cropPadding", publicName: "cropPadding", isSignal: true, isRequired: false, transformFunction: null }, aspectRatio: { classPropertyName: "aspectRatio", publicName: "aspectRatio", isSignal: true, isRequired: false, transformFunction: null }, minZoom: { classPropertyName: "minZoom", publicName: "minZoom", isSignal: true, isRequired: false, transformFunction: null }, maxZoom: { classPropertyName: "maxZoom", publicName: "maxZoom", isSignal: true, isRequired: false, transformFunction: null }, zoomSensitivity: { classPropertyName: "zoomSensitivity", publicName: "zoomSensitivity", isSignal: true, isRequired: false, transformFunction: null }, keyboardStep: { classPropertyName: "keyboardStep", publicName: "keyboardStep", isSignal: true, isRequired: false, transformFunction: null }, zoom: { classPropertyName: "zoom", publicName: "zoom", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onCropChange: "onCropChange", onZoomChange: "onZoomChange" }, host: { attributes: { "tabindex": "0", "role": "application" }, listeners: { "mousedown": "onMouseDown($event)", "keydown": "onKeyDown($event)", "keyup": "onKeyUp($event)" }, properties: { "attr.aria-label": "\"Interactive image cropper\"", "attr.aria-describedby": "descriptionId()", "attr.aria-valuemin": "minZoom()", "attr.aria-valuemax": "maxZoom()", "attr.aria-valuenow": "effectiveZoom()", "attr.aria-valuetext": "zoomValueText()" } }, providers: [provideToken(CROPPER_ROOT_CONTEXT, RdxCropperRootDirective)], ngImport: i0 }); }
608
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: RdxCropperRootDirective, isStandalone: true, selector: "[rdxCropperRoot]", inputs: { image: { classPropertyName: "image", publicName: "image", isSignal: true, isRequired: true, transformFunction: null }, cropPadding: { classPropertyName: "cropPadding", publicName: "cropPadding", isSignal: true, isRequired: false, transformFunction: null }, aspectRatio: { classPropertyName: "aspectRatio", publicName: "aspectRatio", isSignal: true, isRequired: false, transformFunction: null }, minZoom: { classPropertyName: "minZoom", publicName: "minZoom", isSignal: true, isRequired: false, transformFunction: null }, maxZoom: { classPropertyName: "maxZoom", publicName: "maxZoom", isSignal: true, isRequired: false, transformFunction: null }, zoomSensitivity: { classPropertyName: "zoomSensitivity", publicName: "zoomSensitivity", isSignal: true, isRequired: false, transformFunction: null }, keyboardStep: { classPropertyName: "keyboardStep", publicName: "keyboardStep", isSignal: true, isRequired: false, transformFunction: null }, zoomKeyboardStep: { classPropertyName: "zoomKeyboardStep", publicName: "zoomKeyboardStep", isSignal: true, isRequired: false, transformFunction: null }, zoom: { classPropertyName: "zoom", publicName: "zoom", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { onCropChange: "onCropChange", onZoomChange: "onZoomChange" }, host: { attributes: { "role": "application" }, properties: { "attr.tabindex": "disabled() ? -1 : 0", "attr.aria-label": "ariaLabel()", "attr.aria-describedby": "descriptionId", "attr.aria-disabled": "disabled() ? \"true\" : undefined", "attr.data-disabled": "disabled() ? \"\" : undefined", "attr.data-dragging": "isDragging() ? \"\" : undefined" } }, providers: [provideCropperRootContext(rootContext)], ngImport: i0 }); }
629
609
  }
630
610
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: RdxCropperRootDirective, decorators: [{
631
611
  type: Directive,
632
612
  args: [{
633
613
  selector: '[rdxCropperRoot]',
634
- providers: [provideToken(CROPPER_ROOT_CONTEXT, RdxCropperRootDirective)],
614
+ providers: [provideCropperRootContext(rootContext)],
635
615
  host: {
636
- '[attr.aria-label]': '"Interactive image cropper"',
637
- '[attr.aria-describedby]': 'descriptionId()',
638
- '[attr.aria-valuemin]': 'minZoom()',
639
- '[attr.aria-valuemax]': 'maxZoom()',
640
- '[attr.aria-valuenow]': 'effectiveZoom()',
641
- '[attr.aria-valuetext]': 'zoomValueText()',
642
- tabindex: '0',
616
+ // `application` so a screen reader passes arrow/+/- keys straight to our handlers (pan + zoom)
617
+ // instead of intercepting them for browse-mode navigation; instructions come via the
618
+ // description element referenced by `aria-describedby`. The slider-style `aria-value*`
619
+ // attributes were removed — they are only honored on range roles, so they were dead here.
643
620
  role: 'application',
644
- '(mousedown)': 'onMouseDown($event)',
645
- '(keydown)': 'onKeyDown($event)',
646
- '(keyup)': 'onKeyUp($event)'
621
+ '[attr.tabindex]': 'disabled() ? -1 : 0',
622
+ '[attr.aria-label]': 'ariaLabel()',
623
+ '[attr.aria-describedby]': 'descriptionId',
624
+ '[attr.aria-disabled]': 'disabled() ? "true" : undefined',
625
+ '[attr.data-disabled]': 'disabled() ? "" : undefined',
626
+ '[attr.data-dragging]': 'isDragging() ? "" : undefined'
647
627
  }
648
628
  }]
649
- }], ctorParameters: () => [], propDecorators: { image: [{ type: i0.Input, args: [{ isSignal: true, alias: "image", required: true }] }], cropPadding: [{ type: i0.Input, args: [{ isSignal: true, alias: "cropPadding", required: false }] }], aspectRatio: [{ type: i0.Input, args: [{ isSignal: true, alias: "aspectRatio", required: false }] }], minZoom: [{ type: i0.Input, args: [{ isSignal: true, alias: "minZoom", required: false }] }], maxZoom: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxZoom", required: false }] }], zoomSensitivity: [{ type: i0.Input, args: [{ isSignal: true, alias: "zoomSensitivity", required: false }] }], keyboardStep: [{ type: i0.Input, args: [{ isSignal: true, alias: "keyboardStep", required: false }] }], zoom: [{ type: i0.Input, args: [{ isSignal: true, alias: "zoom", required: false }] }], onCropChange: [{ type: i0.Output, args: ["onCropChange"] }], onZoomChange: [{ type: i0.Output, args: ["onZoomChange"] }] } });
629
+ }], ctorParameters: () => [], propDecorators: { image: [{ type: i0.Input, args: [{ isSignal: true, alias: "image", required: true }] }], cropPadding: [{ type: i0.Input, args: [{ isSignal: true, alias: "cropPadding", required: false }] }], aspectRatio: [{ type: i0.Input, args: [{ isSignal: true, alias: "aspectRatio", required: false }] }], minZoom: [{ type: i0.Input, args: [{ isSignal: true, alias: "minZoom", required: false }] }], maxZoom: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxZoom", required: false }] }], zoomSensitivity: [{ type: i0.Input, args: [{ isSignal: true, alias: "zoomSensitivity", required: false }] }], keyboardStep: [{ type: i0.Input, args: [{ isSignal: true, alias: "keyboardStep", required: false }] }], zoomKeyboardStep: [{ type: i0.Input, args: [{ isSignal: true, alias: "zoomKeyboardStep", required: false }] }], zoom: [{ type: i0.Input, args: [{ isSignal: true, alias: "zoom", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], onCropChange: [{ type: i0.Output, args: ["onCropChange"] }], onZoomChange: [{ type: i0.Output, args: ["onZoomChange"] }] } });
650
630
 
651
631
  const _imports = [
652
632
  RdxCropperRootDirective,
@@ -677,5 +657,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImpor
677
657
  * Generated bundle index. Do not edit.
678
658
  */
679
659
 
680
- export { CROPPER_ROOT_CONTEXT, RdxCropperCropAreaDirective, RdxCropperDescriptionDirective, RdxCropperImageComponent, RdxCropperModule, RdxCropperRootDirective, injectCropperRootContext };
660
+ export { RdxCropperCropAreaDirective, RdxCropperDescriptionDirective, RdxCropperImageComponent, RdxCropperModule, RdxCropperRootDirective, injectCropperRootContext, provideCropperRootContext };
681
661
  //# sourceMappingURL=radix-ng-primitives-cropper.mjs.map