kyd-shared-badge 0.2.29 → 0.2.30

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kyd-shared-badge",
3
- "version": "0.2.29",
3
+ "version": "0.2.30",
4
4
  "private": false,
5
5
  "main": "./src/index.ts",
6
6
  "module": "./src/index.ts",
@@ -20,7 +20,7 @@ import Skills from './components/Skills';
20
20
  import CategoryBars from './components/CategoryBars';
21
21
  import SkillsAppendixTable from './components/SkillsAppendixTable';
22
22
  import { BusinessRulesProvider } from './components/BusinessRulesContext';
23
- import AnimateOnMount from './components/AnimateOnMount';
23
+ import Reveal from './components/Reveal';
24
24
 
25
25
  // const hexToRgba = (hex: string, alpha: number) => {
26
26
  // const clean = hex.replace('#', '');
@@ -139,7 +139,7 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
139
139
  <BusinessRulesProvider items={graphInsights?.business_rules_all}>
140
140
  <div className={`${wrapperMaxWidth} mx-auto`}>
141
141
  {/* Share controls removed; app-level pages render their own actions */}
142
- <AnimateOnMount delayMs={40} offsetY={8}>
142
+ <Reveal offsetY={8} durationMs={500}>
143
143
  <ReportHeader
144
144
  badgeId={badgeId}
145
145
  developerName={badgeData.developerName}
@@ -150,16 +150,15 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
150
150
  summary={report_summary}
151
151
  countries={(assessmentResult?.screening_sources?.ip_risk_analysis?.raw_data?.countries) || []}
152
152
  />
153
- </AnimateOnMount>
154
- <AnimateOnMount delayMs={80} offsetY={10}>
155
- <div
156
- className={'rounded-xl shadow-xl p-6 sm:p-8 mt-8 border'}
157
- style={{ backgroundColor: 'var(--content-card-background)', borderColor: 'var(--icon-button-secondary)' }}
158
- >
153
+ </Reveal>
154
+ <div
155
+ className={'rounded-xl shadow-xl p-6 sm:p-8 mt-8 border'}
156
+ style={{ backgroundColor: 'var(--content-card-background)', borderColor: 'var(--icon-button-secondary)' }}
157
+ >
159
158
 
160
159
  <div className={'space-y-12 divide-y'} style={{ borderColor: 'var(--icon-button-secondary)' }}>
161
160
  <div className="pt-8 first:pt-0">
162
- <h4 className={'text-2xl font-semibold mb-3'} style={{ color: 'var(--text-main)' }}>Report Summary</h4>
161
+ <Reveal as={'h4'} offsetY={8} durationMs={500} className={'text-2xl font-semibold mb-3'} style={{ color: 'var(--text-main)' }}>Report Summary</Reveal>
163
162
  <div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
164
163
  {/* Technical semicircle gauge (refactored) */}
165
164
  {(() => {
@@ -168,7 +167,7 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
168
167
  const label = ui?.label || 'EVIDENCE';
169
168
  const top = ui?.top_movers && ui.top_movers.length > 0 ? ui.top_movers : topBusinessForGenre('Technical');
170
169
  return (
171
- <AnimateOnMount delayMs={120} offsetY={8}>
170
+ <Reveal delayMs={0} offsetY={12}>
172
171
  <GaugeCard
173
172
  key={'technical-card'}
174
173
  title={'KYD Technical'}
@@ -178,7 +177,7 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
178
177
  topMovers={top?.map(t => ({ label: t?.label, uid: t?.uid }))}
179
178
  topMoversTitle={'Top Score Movers'}
180
179
  />
181
- </AnimateOnMount>
180
+ </Reveal>
182
181
  );
183
182
  })()}
184
183
 
@@ -190,7 +189,7 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
190
189
  const top = ui?.top_movers && ui.top_movers.length > 0 ? ui.top_movers : topBusinessForGenre('Risk');
191
190
  const tooltip = 'Higher bar filled indicates lower overall risk; movement to the right reflects improved risk posture.';
192
191
  return (
193
- <AnimateOnMount delayMs={160} offsetY={8}>
192
+ <Reveal delayMs={80} offsetY={12}>
194
193
  <RiskCard
195
194
  title={'KYD Risk'}
196
195
  description={'The bar chart visualizes relative risk levels, where shorter bars denote lower risk and taller bars indicate greater exposure.'}
@@ -200,7 +199,7 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
200
199
  topMoversTitle={'Top Score Movers'}
201
200
  tooltipText={tooltip}
202
201
  />
203
- </AnimateOnMount>
202
+ </Reveal>
204
203
  );
205
204
  })()}
206
205
 
@@ -210,7 +209,7 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
210
209
  const label = 'AI Transparency'// TODO: calculate label frontend
211
210
  const topMovers = ai_usage_summary?.key_findings || []
212
211
  return (
213
- <AnimateOnMount delayMs={200} offsetY={8}>
212
+ <Reveal delayMs={160} offsetY={12}>
214
213
  <GaugeCard
215
214
  key={'ai-card'}
216
215
  title={'KYD AI'}
@@ -221,7 +220,7 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
221
220
  topMovers={topMovers.map(t => ({ label: t, uid: 'ai-usage' }))}
222
221
  topMoversTitle={'Key Findings'}
223
222
  />
224
- </AnimateOnMount>
223
+ </Reveal>
225
224
  );
226
225
  })()}
227
226
  </div>
@@ -230,9 +229,9 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
230
229
  {/* Technical Scores */}
231
230
  <div className="mt-8" >
232
231
  <div key={'Technical'} className='pt-8 space-y-8' style={{ borderColor: 'var(--icon-button-secondary)'}}>
233
- <h4 className={'text-2xl font-semibold mb-3'} style={{ color: 'var(--text-main)' }}>KYD Technical</h4>
232
+ <Reveal as={'h4'} offsetY={8} className={'text-2xl font-semibold mb-3'} style={{ color: 'var(--text-main)' }}>KYD Technical</Reveal>
234
233
  {/* technical graph insights */}
235
- <AnimateOnMount delayMs={120} offsetY={8}>
234
+ <Reveal>
236
235
  <div className="">
237
236
  <GraphInsights
238
237
  graphInsights={graphInsights}
@@ -241,14 +240,13 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
241
240
  scoringSummary={scoringSummary}
242
241
  />
243
242
  </div>
244
- </AnimateOnMount>
243
+ </Reveal>
245
244
 
246
245
  {/* category bars and contributing factors */}
247
- <AnimateOnMount delayMs={160} offsetY={10}>
248
246
  <div className="grid grid-cols-1 lg:grid-cols-12 w-full gap-8 items-stretch py-8 border-t" style={{ borderColor: 'var(--icon-button-secondary)'}}>
249
247
 
250
248
  {/* Left: Bars */}
251
- <div className="lg:col-span-8 h-full">
249
+ <Reveal className="lg:col-span-8 h-full">
252
250
  <CategoryBars
253
251
  title={'Technical Category Contributions - Percentages'}
254
252
  categories={genreMapping?.['Technical'] as string[]}
@@ -257,10 +255,10 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
257
255
  getCategoryTooltipCopy={getCategoryTooltipCopy}
258
256
  barHeight={16}
259
257
  />
260
- </div>
258
+ </Reveal>
261
259
 
262
260
  {/* Right: Contributing Factors */}
263
- <div className="lg:col-span-4 w-full ml-20 flex flex-col items-start justify-start">
261
+ <Reveal className="lg:col-span-4 w-full ml-20 flex flex-col items-start justify-start" delayMs={80}>
264
262
  <div className={'text-sm font-semibold mb-3'} style={{ color: 'var(--text-main)' }}>Top Contributing Factors</div>
265
263
  <div className="space-y-4">
266
264
  {(genreMapping?.['Technical'] || []).map((cat: string) => {
@@ -289,30 +287,28 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
289
287
  );
290
288
  })}
291
289
  </div>
292
- </div>
290
+ </Reveal>
293
291
  </div>
294
- </AnimateOnMount>
295
292
 
296
- <AnimateOnMount delayMs={180} offsetY={10}>
293
+ <Reveal>
297
294
  <div className="pt-8 border-t" style={{ borderColor: 'var(--icon-button-secondary)'}}>
298
295
  <h3 className={'text-xl font-bold mb-3'} style={{ color: 'var(--text-main)' }}>KYD Technical - Skills Insights</h3>
299
296
  <div className={'prose prose-sm max-w-none mb-6 space-y-4'} style={{ color: 'var(--text-secondary)' }}>
300
297
  <Skills skillsMatrix={skillsMatrix} skillsCategoryRadar={graphInsights?.skillsCategoryRadar} />
301
298
  </div>
302
299
  </div>
303
- </AnimateOnMount>
300
+ </Reveal>
304
301
 
305
302
  </div>
306
303
  </div>
307
304
 
308
305
 
309
306
 
310
- <AnimateOnMount delayMs={200} offsetY={10}>
311
307
  <div className="pt-8 space-y-8">
312
- <h3 className={'text-2xl font-bold'} style={{ color: 'var(--text-main)' }}>KYD Risk - Overview</h3>
308
+ <Reveal as={'h3'} offsetY={8} className={'text-2xl font-bold'} style={{ color: 'var(--text-main)' }}>KYD Risk - Overview</Reveal>
313
309
 
314
310
  {/* Risk Graph Insights and Category Bars */}
315
- <AnimateOnMount delayMs={220} offsetY={8}>
311
+ <Reveal>
316
312
  <div className="">
317
313
  <GraphInsights
318
314
  graphInsights={graphInsights}
@@ -321,11 +317,10 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
321
317
  scoringSummary={scoringSummary}
322
318
  />
323
319
  </div>
324
- </AnimateOnMount>
325
- <AnimateOnMount delayMs={240} offsetY={10}>
320
+ </Reveal>
326
321
  <div className="grid grid-cols-1 lg:grid-cols-12 gap-8 w-full items-stretch py-8 border-y" style={{ borderColor: 'var(--icon-button-secondary)' }}>
327
322
  {/* Left: Bars */}
328
- <div className="lg:col-span-8 h-full">
323
+ <Reveal className="lg:col-span-8 h-full">
329
324
  <CategoryBars
330
325
  title={'KYD Risk - Category Insights'}
331
326
  categories={genreMapping?.['Risk'] as string[]}
@@ -334,9 +329,9 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
334
329
  getCategoryTooltipCopy={getCategoryTooltipCopy}
335
330
  barHeight={16}
336
331
  />
337
- </div>
332
+ </Reveal>
338
333
  {/* Right: Contributing Factors */}
339
- <div className="lg:col-span-4 w-full ml-20 flex flex-col items-start justify-start">
334
+ <Reveal className="lg:col-span-4 w-full ml-20 flex flex-col items-start justify-start" delayMs={80}>
340
335
  <div className={'text-sm font-semibold mb-3'} style={{ color: 'var(--text-main)' }}>Top Contributing Factors</div>
341
336
  <div className="space-y-4">
342
337
  {genreMapping?.['Risk']?.map((cat: string) => {
@@ -365,13 +360,12 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
365
360
  );
366
361
  })}
367
362
  </div>
368
- </div>
363
+ </Reveal>
369
364
  </div>
370
- </AnimateOnMount>
371
365
 
372
366
  {/* cyber risk display */}
373
367
  {badgeData.optOutScreening ? (
374
- <div className={'p-4 rounded-lg border'} style={{ backgroundColor: 'var(--icon-button-secondary)', borderColor: 'var(--icon-button-secondary)' }}>
368
+ <Reveal className={'p-4 rounded-lg border'} style={{ backgroundColor: 'var(--icon-button-secondary)', borderColor: 'var(--icon-button-secondary)' }}>
375
369
  <div className="flex items-start">
376
370
  <span className="h-5 w-5 mr-3 mt-0.5 flex-shrink-0" style={{ color: yellow }}>
377
371
  <FiAlertTriangle size={20} />
@@ -383,7 +377,7 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
383
377
  </p>
384
378
  </div>
385
379
  </div>
386
- </div>
380
+ </Reveal>
387
381
  ) : (
388
382
  <>
389
383
  {(() => {
@@ -393,8 +387,7 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
393
387
  const fbiMatches = ss?.fbi_matches && (ss.fbi_matches.length > 0);
394
388
  if (!(ofacMatches || cslDetails || fbiMatches)) return null;
395
389
  return (
396
- <AnimateOnMount delayMs={260} offsetY={8}>
397
- <div className={'mb-8 rounded-lg border p-4'} style={{ borderColor: 'var(--icon-button-secondary)', backgroundColor: 'var(--content-card-background)' }}>
390
+ <Reveal className={'mb-8 rounded-lg border p-4'} style={{ borderColor: 'var(--icon-button-secondary)', backgroundColor: 'var(--content-card-background)' }}>
398
391
  <h4 className={'text-lg font-semibold mb-3'} style={{ color: 'var(--text-main)' }}>3A. Sanctions Matches</h4>
399
392
  {/* OFAC matches */}
400
393
  {ofacMatches && (
@@ -454,41 +447,38 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
454
447
  </div>
455
448
  </div>
456
449
  )}
457
- </div>
458
- </AnimateOnMount>
450
+ </Reveal>
459
451
  );
460
452
  })()}
461
- <AnimateOnMount delayMs={280} offsetY={8}>
453
+ <Reveal>
462
454
  <IpRiskAnalysisDisplay ipRiskAnalysis={screening_sources?.ip_risk_analysis} />
463
- </AnimateOnMount>
455
+ </Reveal>
464
456
  </>
465
457
  )}
466
458
 
467
459
  </div>
468
- </AnimateOnMount>
469
460
 
470
461
  {/* Connected Platforms */}
471
- <AnimateOnMount delayMs={260} offsetY={10}>
462
+ <Reveal>
472
463
  <ConnectedPlatforms accounts={connected} />
473
- </AnimateOnMount>
464
+ </Reveal>
474
465
 
475
466
 
476
- <AnimateOnMount delayMs={280} offsetY={10}>
477
467
  <div className="pt-8">
478
468
  <h3 className={'text-2xl font-bold mb-4'} style={{ color: 'var(--text-main)' }}>Appendix: Data Sources</h3>
479
469
  <div className="space-y-8">
480
470
 
481
471
  {/* Skills */}
482
- <AnimateOnMount delayMs={300} offsetY={8}>
472
+ <Reveal>
483
473
  <div>
484
474
  <h4 id="appendix-skills" className={'text-lg font-bold mb-4'} style={{ color: 'var(--text-main)' }}>Skills</h4>
485
475
  <SkillsAppendixTable skillsAll={skillsAll} />
486
476
  </div>
487
- </AnimateOnMount>
477
+ </Reveal>
488
478
 
489
479
  {/* Observations */}
490
480
  {Array.isArray(graphInsights?.business_rules_all) && graphInsights.business_rules_all.length > 0 && (
491
- <AnimateOnMount delayMs={320} offsetY={8}>
481
+ <Reveal>
492
482
  <div>
493
483
  <h4 className={'text-lg font-bold mb-4'} style={{ color: 'var(--text-main)' }}>Observations</h4>
494
484
  <AppendixTables
@@ -499,12 +489,12 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
499
489
  developerName={developerName || 'this developer'}
500
490
  />
501
491
  </div>
502
- </AnimateOnMount>
492
+ </Reveal>
503
493
  )}
504
494
 
505
495
  {/* Sanctions & Watchlists */}
506
496
  {!badgeData.optOutScreening && screening_sources && (
507
- <AnimateOnMount delayMs={340} offsetY={8}>
497
+ <Reveal>
508
498
  <div>
509
499
  <h4 className={'text-lg font-bold mb-4'} style={{ color: 'var(--text-main)' }}>Sanctions & Watchlists</h4>
510
500
  {(() => {
@@ -527,15 +517,14 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
527
517
  />
528
518
  );
529
519
  })()}
530
- </div>
531
- </AnimateOnMount>
520
+ </div>
521
+ </Reveal>
532
522
  )}
533
523
 
534
524
  </div>
535
525
  </div>
536
- </AnimateOnMount>
537
526
 
538
- <AnimateOnMount delayMs={300} offsetY={6}>
527
+ <Reveal>
539
528
  <div className={'pt-8 text-sm text-center'} style={{ color: 'var(--text-secondary)' }}>
540
529
  Report Completed: {new Date(updatedAt).toLocaleString(undefined, {
541
530
  year: 'numeric',
@@ -546,15 +535,16 @@ const SharedBadgeDisplay = ({ badgeData }: { badgeData: PublicBadgeData }) => {
546
535
  timeZoneName: 'short',
547
536
  })}
548
537
  </div>
549
- </AnimateOnMount>
538
+ </Reveal>
550
539
  </div>
551
540
  </div>
552
- </AnimateOnMount>
553
- <footer className={'mt-12 pt-6 border-t'} style={{ borderColor: 'var(--icon-button-secondary)' }}>
554
- <p className={'text-center text-xs max-w-4xl mx-auto'} style={{ color: 'var(--text-secondary)' }}>
555
- © 2025 Know Your Developer, LLC. All rights reserved. KYD Self-Check™, and associated marks are trademarks of Know Your Developer, LLC. This document is confidential, proprietary, and intended solely for the individual or entity to whom it is addressed. Unauthorized use, disclosure, copying, or distribution of this document or any of its contents is strictly prohibited and may be unlawful. Know Your Developer, LLC assumes no responsibility or liability for any errors or omissions contained herein. Report validity subject to the terms and conditions stated on the official Know Your Developer website located at https://knowyourdeveloper.ai.
556
- </p>
557
- </footer>
541
+ <Reveal>
542
+ <footer className={'mt-12 pt-6 border-t'} style={{ borderColor: 'var(--icon-button-secondary)' }}>
543
+ <p className={'text-center text-xs max-w-4xl mx-auto'} style={{ color: 'var(--text-secondary)' }}>
544
+ © 2025 Know Your Developer, LLC. All rights reserved. KYD Self-Check™, and associated marks are trademarks of Know Your Developer, LLC. This document is confidential, proprietary, and intended solely for the individual or entity to whom it is addressed. Unauthorized use, disclosure, copying, or distribution of this document or any of its contents is strictly prohibited and may be unlawful. Know Your Developer, LLC assumes no responsibility or liability for any errors or omissions contained herein. Report validity subject to the terms and conditions stated on the official Know Your Developer website located at https://knowyourdeveloper.ai.
545
+ </p>
546
+ </footer>
547
+ </Reveal>
558
548
  </div>
559
549
  </BusinessRulesProvider>
560
550
  );
@@ -0,0 +1,62 @@
1
+ 'use client';
2
+
3
+ import React, { CSSProperties, PropsWithChildren } from 'react';
4
+ import { useInViewOnce } from './useInViewOnce';
5
+
6
+ type RevealProps = PropsWithChildren<{
7
+ as?: keyof JSX.IntrinsicElements;
8
+ /** px to translate on Y axis initially */
9
+ offsetY?: number;
10
+ /** px to translate on X axis initially (rarely needed) */
11
+ offsetX?: number;
12
+ /** ms */
13
+ durationMs?: number;
14
+ /** ms */
15
+ delayMs?: number;
16
+ /** Optional className passed to the wrapper */
17
+ className?: string;
18
+ /** Optional style merged into wrapper */
19
+ style?: CSSProperties;
20
+ /** Intersection thresholds */
21
+ threshold?: number | number[];
22
+ /** Root margin for preloading */
23
+ rootMargin?: string;
24
+ }>;
25
+
26
+ const Reveal: React.FC<RevealProps> = ({
27
+ as = 'div',
28
+ offsetY = 12,
29
+ offsetX = 0,
30
+ durationMs = 500,
31
+ delayMs = 0,
32
+ className,
33
+ style,
34
+ threshold,
35
+ rootMargin,
36
+ children,
37
+ }) => {
38
+ const Tag = as as any;
39
+ const { ref, hasIntersected } = useInViewOnce({ threshold, rootMargin });
40
+
41
+ // Respect reduced motion is handled in the hook by triggering immediately.
42
+ const baseTransform = `translate(${offsetX}px, ${offsetY}px)`;
43
+ const eased = 'cubic-bezier(0.22, 1, 0.36, 1)'; // subtle spring-ish ease
44
+
45
+ const inlineStyles: CSSProperties = {
46
+ opacity: hasIntersected ? 1 : 0,
47
+ transform: hasIntersected ? 'none' : baseTransform,
48
+ transition: `opacity ${durationMs}ms ${eased} ${delayMs}ms, transform ${durationMs}ms ${eased} ${delayMs}ms`,
49
+ willChange: hasIntersected ? undefined : 'opacity, transform',
50
+ ...style,
51
+ };
52
+
53
+ return (
54
+ <Tag ref={ref} className={className} style={inlineStyles}>
55
+ {children}
56
+ </Tag>
57
+ );
58
+ };
59
+
60
+ export default Reveal;
61
+
62
+
@@ -0,0 +1,58 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useRef, useState } from 'react';
4
+
5
+ /**
6
+ * Observes the provided element and returns true the first time it enters the viewport.
7
+ * Uses IntersectionObserver under the hood and disconnects after first intersection.
8
+ */
9
+ export function useInViewOnce<T extends HTMLElement>(options?: IntersectionObserverInit) {
10
+ const targetRef = useRef<T | null>(null);
11
+ const [hasIntersected, setHasIntersected] = useState<boolean>(false);
12
+ const [hasMounted, setHasMounted] = useState<boolean>(false);
13
+
14
+ useEffect(() => {
15
+ setHasMounted(true);
16
+ }, []);
17
+
18
+ useEffect(() => {
19
+ if (hasIntersected) return; // already observed once
20
+ const element = targetRef.current;
21
+ if (!element) return;
22
+
23
+ // Respect reduced motion
24
+ const prefersReducedMotion = typeof window !== 'undefined' &&
25
+ window.matchMedia &&
26
+ window.matchMedia('(prefers-reduced-motion: reduce)').matches;
27
+ if (prefersReducedMotion) {
28
+ setHasIntersected(true);
29
+ return;
30
+ }
31
+
32
+ const observer = new IntersectionObserver(
33
+ (entries, obs) => {
34
+ for (const entry of entries) {
35
+ if (entry.isIntersecting) {
36
+ setHasIntersected(true);
37
+ obs.disconnect();
38
+ break;
39
+ }
40
+ }
41
+ },
42
+ {
43
+ root: options?.root || null,
44
+ rootMargin: options?.rootMargin ?? '0px 0px -10% 0px',
45
+ threshold: options?.threshold ?? 0.1,
46
+ }
47
+ );
48
+
49
+ observer.observe(element);
50
+ return () => {
51
+ observer.disconnect();
52
+ };
53
+ }, [options?.root, options?.rootMargin, options?.threshold, hasIntersected]);
54
+
55
+ return { ref: targetRef, isVisible: hasIntersected || !hasMounted ? false : hasIntersected, hasIntersected } as const;
56
+ }
57
+
58
+
@@ -1,66 +0,0 @@
1
- 'use client';
2
-
3
- import React from 'react';
4
-
5
- type AnimateOnMountProps = {
6
- children: React.ReactNode;
7
- delayMs?: number;
8
- durationMs?: number;
9
- offsetY?: number; // positive moves up during animation
10
- className?: string;
11
- style?: React.CSSProperties;
12
- as?: 'div' | 'section' | 'span' | 'header' | 'footer' | 'main' | 'article' | 'aside' | 'nav';
13
- };
14
-
15
- const AnimateOnMount: React.FC<AnimateOnMountProps> = ({
16
- children,
17
- delayMs = 60,
18
- durationMs = 420,
19
- offsetY = 10,
20
- className,
21
- style,
22
- as = 'div',
23
- }) => {
24
- const [mounted, setMounted] = React.useState(false);
25
- const reduceMotion = React.useMemo(() => {
26
- if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return false;
27
- try {
28
- return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
29
- } catch {
30
- return false;
31
- }
32
- }, []);
33
-
34
- React.useEffect(() => {
35
- if (reduceMotion) {
36
- // Render immediately with no animation
37
- setMounted(true);
38
- return;
39
- }
40
- const id = requestAnimationFrame(() => setMounted(true));
41
- return () => cancelAnimationFrame(id);
42
- }, [reduceMotion]);
43
-
44
- const Component = as as any;
45
- const transition = `opacity ${durationMs}ms ease, transform ${durationMs}ms ease`;
46
-
47
- return (
48
- <Component
49
- className={className}
50
- style={{
51
- opacity: mounted ? 1 : 0,
52
- transform: mounted || reduceMotion ? 'translateY(0px)' : `translateY(${offsetY}px)`,
53
- transition: reduceMotion ? undefined : transition,
54
- transitionDelay: reduceMotion ? undefined : `${delayMs}ms`,
55
- willChange: reduceMotion ? undefined : 'opacity, transform',
56
- ...style,
57
- }}
58
- >
59
- {children}
60
- </Component>
61
- );
62
- };
63
-
64
- export default AnimateOnMount;
65
-
66
-