Repository: siluat/web-projects
Pull request #13: ✨ (ui-craft) Add FFXIV job icons loop component using flubber
Author: login: siluat
Created: 2025-03-25T12:08:04Z | Merged: 2025-03-25T12:21:32Z



<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
	- 게임 아이콘 전환 방식이 개선되어, SVG 경로 모핑 애니메이션을 통한 더욱 부드럽고 역동적인 전환 효과가 추가되었습니다.
	- 데모 페이지가 업데이트되어, 새로운 애니메이션 디자인을 직접 확인할 수 있습니다.
	- `useIncrementingTuple` 훅이 추가되어, 아이콘 전환 시 인덱스를 순환적으로 관리합니다.
- **Tests**
	- `useIncrementingTuple` 훅에 대한 새로운 테스트 케이스가 추가되었습니다.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/packages/ui-craft/package.json b/packages/ui-craft/package.json
index 9ad6ade..bf74633 100644
--- a/packages/ui-craft/package.json
+++ b/packages/ui-craft/package.json
@@ -14,6 +14,7 @@
   "devDependencies": {
     "@siluat/typescript-config": "workspace:*",
     "@testing-library/react": "^16.2.0",
+    "@types/flubber": "^0.4.0",
     "@types/react": "^19.0.11",
     "@types/react-dom": "^19.0.4",
     "@vitejs/plugin-react": "^4.3.4",
@@ -26,5 +27,9 @@
   "peerDependencies": {
     "react": "^19.0.0",
     "react-dom": "^19.0.0"
+  },
+  "dependencies": {
+    "flubber": "^0.4.2",
+    "framer-motion": "^12.5.0"
   }
 }
diff --git a/packages/ui-craft/react/ffxiv-job-icons-loop-using-flubber.test.tsx b/packages/ui-craft/react/ffxiv-job-icons-loop-using-flubber.test.tsx
new file mode 100644
index 0000000..557cfa0
--- /dev/null
+++ b/packages/ui-craft/react/ffxiv-job-icons-loop-using-flubber.test.tsx
@@ -0,0 +1,49 @@
+import { act, renderHook } from '@testing-library/react';
+import { useIncrementingTuple } from './ffxiv-job-icons-loop-using-flubber.js';
+
+describe('useIncrementingTuple', () => {
+  test('size <= 1 should always return [0, 0]', () => {
+    const { result } = renderHook(() => useIncrementingTuple(1));
+    expect(result.current[0]).toEqual([0, 0]);
+
+    act(() => {
+      result.current[1]();
+    });
+    expect(result.current[0]).toEqual([0, 0]);
+  });
+
+  test('size = 2 should toggle between [0, 1] and [1, 0]', () => {
+    const { result } = renderHook(() => useIncrementingTuple(2));
+
+    expect(result.current[0]).toEqual([0, 1]);
+
+    act(() => {
+      result.current[1]();
+    });
+    expect(result.current[0]).toEqual([1, 0]);
+
+    act(() => {
+      result.current[1]();
+    });
+    expect(result.current[0]).toEqual([0, 1]);
+  });
+
+  test('increment function updates the tuple correctly for size > 2', () => {
+    const { result } = renderHook(() => useIncrementingTuple(3));
+
+    act(() => {
+      result.current[1]();
+    });
+    expect(result.current[0]).toEqual([1, 2]);
+
+    act(() => {
+      result.current[1]();
+    });
+    expect(result.current[0]).toEqual([2, 0]);
+
+    act(() => {
+      result.current[1]();
+    });
+    expect(result.current[0]).toEqual([0, 1]);
+  });
+});
diff --git a/packages/ui-craft/react/ffxiv-job-icons-loop-using-flubber.tsx b/packages/ui-craft/react/ffxiv-job-icons-loop-using-flubber.tsx
new file mode 100644
index 0000000..3a3179c
--- /dev/null
+++ b/packages/ui-craft/react/ffxiv-job-icons-loop-using-flubber.tsx
@@ -0,0 +1,122 @@
+'use client';
+
+import { interpolate } from 'flubber';
+import { animate } from 'framer-motion';
+import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
+
+export function FfxivJobIconsLoopUsingFlubber() {
+  const path = useSvgPathMorphing(ffxivJobIconPaths);
+  return (
+    <svg viewBox={ffxivJobIconData.viewBox} width={80} height={80}>
+      <title>FFXIV Job Icons Loop</title>
+      <g fill="currentColor">
+        <path d={path} />
+      </g>
+    </svg>
+  );
+}
+
+function useSvgPathMorphing(pathList: string[]) {
+  if (pathList.length < 2) {
+    throw new Error('useSvgPathMorphing: pathList length must be at least 2');
+  }
+
+  const [[from, to], increment] = useIncrementingTuple(pathList.length);
+
+  const interpolater = useMemo(() => {
+    const fromPath = pathList[from];
+    const toPath = pathList[to];
+    if (!fromPath || !toPath) {
+      throw new Error('Invalid path index');
+    }
+    return interpolate(fromPath, toPath);
+  }, [from, to, pathList]);
+
+  const [currentPath, setCurrentPath] = useState(pathList[from]);
+
+  useLayoutEffect(() => {
+    const animation = animate<number>(0, 1, {
+      duration: 0.2,
+      delay: 0.3,
+      ease: 'easeIn',
+      onUpdate: (progress) => {
+        setCurrentPath(interpolater(progress));
+      },
+      onComplete: () => {
+        increment();
+      },
+    });
+    return () => animation.stop();
+  }, [increment, interpolater]);
+
+  return currentPath;
+}
+
+export function useIncrementingTuple(
+  size: number,
+): [[number, number], () => void] {
+  const [tuple, setTuple] = useState<[number, number]>(
+    size > 1 ? [0, 1] : [0, 0],
+  );
+
+  const increment = useCallback(() => {
+    if (size <= 1) return;
+
+    setTuple(([a, b]) => {
+      const nextA = (a + 1) % size;
+      const nextB = (b + 1) % size;
+      return [nextA, nextB];
+    });
+  }, [size]);
+
+  return [tuple, increment];
+}
+
+const ffxivJobIconData = {
+  viewBox: '0 0 1024 1024',
+  paths: {
+    paladin:
+      'M757 139q-5 0-14 10-6 6-18.5 21.5T707 192q-9 10-14 10H555q-4 0-20-34l-6-13q-6-12-14-20-5-6-11-8l-4-2-4 2q-6 2-11 8-8 8-14 20l-6 13q-16 34-20 34H307q-5 0-14-10-5-6-17.5-21.5T257 149q-9-10-14-10-14 0-26 13-14 14-14 35v258q0 96 33 177 31 76 88 130 48 45 115 88 34 21 61 35 26-13 61-35 67-43 115-88 57-54 88-130 33-81 33-177V187q0-21-14-35-12-13-26-13zM469 768q-11-6-21-12-26-17-49-37-34-29-59-61-31-40-47-94-12-42-12-80l1-219h187v503zm191-110q-25 32-59 61-23 20-49 37l-21 12V265h187l1 219q0 38-12 80-16 54-47 94z',
+    monk: 'M525 235q21-18 36-39 12-17 19-35 6-13 9-26l1-10h-10q-12 1-26 4-20 4-38 13-24 11-45 29L190 407q-20 17-35 38-12 17-20 35-6 13-8 26l-2 10h10q13-1 27-4 19-4 38-13 23-11 44-29l140-118zm142 169q21-17 36-38 12-17 20-36 5-13 8-25l2-10h-10q-13 1-27 4-19 4-38 13-23 11-44 29L333 576q-21 18-36 39-12 17-20 35-5 13-8 26l-2 9h10q13 0 27-3 19-5 38-13 23-11 44-29l141-118zm89 106L616 628 475 746q-21 18-36 39-12 16-19 35-6 13-9 25l-1 10h10q12 0 26-3 20-5 38-14 24-11 45-28l281-236q38-32 55-74 9-21 10-35h-10q-13 0-27 3-19 5-38 13-23 12-44 29z',
+    warrior:
+      'M875 500l-2-26q-3-31-12-63-11-44-31-82-17-31-45.5-62t-57-49-43-14-22.5 12q-11 11-16 32l-4 22q-4 21-9 32-7 17-25 38-22 25-67 33-23 3-41 2-20 1-43-2-44-8-65-33-18-21-25-38-5-11-9-32l-4-22q-5-21-16-31-8-9-22.5-13t-43 14-57 49-45.5 62q-20 38-31 82-9 32-12 64l-2 25q1 24 7 57 11 63 39 114 16 31 45 62t57 49 42.5 14 23.5-12q10-11 15-32l5-22q4-21 8-32 8-17 25-38 16-18 44-27 20-7 44-8h20q21-2 43 2 44 8 65 33 18 21 25 38 5 11 9 32l4 22q5 21 16 31 8 9 22.5 13t43-14.5 57-49T830 670q20-37 31-81 9-32 12-64zM760 627q-9 18-22 35-9 12-18 22l-8 7-3-11q-5-15-12-29-10-19-23-35-16-19-36-32-24-16-51-25-22-7-44-11-16-2-31-2l-12 1q-18-2-45 2-50 8-92 35-37 23-59 68-12 22-16 40l-7-8q-10-10-19-22-12-17-22-35-13-26-20-58-5-24-6-49l-1-20q-1-19 2-44 6-48 25-83 9-18 22-35 9-12 18-22l8-7 3 11q5 15 12 28 10 20 23 36 16 19 36 32 44 28 95 35 26 4 43 2 19 2 44-2 51-8 94-35 20-13 36-33 13-15 23-35 7-14 12-28l3-12 8 8q9 10 18 22 13 17 22 35 19 35 25 82 3 25 2 45v1q1 22-3 46-6 46-24 80z',
+    dragoon:
+      'M723 335h-38l-8 10q-9 12-18 19-11 10-17 10-11 0-18-14t-7-28V171q0-13-7-30-4-13-12-25l-7-11-7 12q-7 13-12 26-7 17-7 28v172l-5 7q-7 8-19 16-19 13-41 16-22-3-41-16t-24-23V171q0-11-7-28-5-13-12-26l-7-12-7 11q-8 12-12 25-7 17-7 30v161q0 14-7 28t-18 14q-6 0-17-10-9-7-18-19l-8-10h-38q-6 0-10.5 4t-3.5 10q0 4 72 165 53 117 58 130 7 19 41 122l19 56q5 13 14 23t16 10l17 1 17-1q7 0 16-10t14-23l19-56q34-103 41-122 5-13 58-130 72-161 72-165 0-6-4-10t-10-4zm-92 142q-5 14-24 35-17 20-34 31l-10 6q-12 7-19.5 12.5t-14 14T523 588t13 20q16 20 16 25v54q0 6-17 30-10 13-11 17-5 10-8 23t-3 19.5-7 10.5l-6 2-6-2q-7-4-7-10.5t-3-19.5-8-23q-1-4-11-17-17-24-17-30v-54q0-5 16-25 13-16 13-20t-6.5-12.5-14-14T437 549l-10-6q-17-11-34-31-19-21-24-35l-10-39 6-9q8-11 16-16 17-11 30-11 2 0 14 12 21 23 33.5 28.5T482 451q8 2 14 2h8q6 0 14-2 11-3 23.5-8.5T575 414q12-12 14-12 13 0 30 11 8 5 16 16l6 9z',
+    bard: 'M368 653V341q0-5-5-9t-12-6l-6-1-6 1q-7 2-12 6t-5 9v312q0 8 12 13 5 2 11 3l6-1q7-2 12-6t5-9zm122 0V341q0-8-11-13-6-2-12-3l-6 1q-6 2-11.5 6t-5.5 9v312q0 5 5.5 9t11.5 6l6 1 6-1q7-2 12-6t5-9zm123 0V341q0-5-5-9t-12-6l-6-1-6 1q-7 2-12 6t-5 9v312q0 5 5 9t12 6l6 1 6-1q7-2 12-6t5-9zm119-538q-17 0-32.5 13.5T684 151t-8 11q-4 2-16 2H299q-18 0-34.5 7.5T235 192q-8 8-14 21.5t-6 23.5q0 20 5 30 8 13 27 13 14 0 25-7 7-4 17-14 7-7 11-9 6-4 11-4h328q16 0 31 10 18 13 18 35v346q0 14-15 38-16 26-41 49-28 27-61 43-39 18-79 18-115 0-195-60l-2-2q-9-6-18-8.5t-14 3 0 23.5 14 28q12 13 35 30 35 27 75 44 52 22 105.5 22T613 829q65-35 109-86 47-55 47-102V304q0-11 8-28.5t8-28.5v-82q0-15-18-32.5T732 115z',
+    whiteMage:
+      'M464 365q27 0 46-19t19-46-19-46.5-46-19.5-46.5 19.5T398 300t19.5 46 46.5 19zm58-239q-13-2-23 1t-12 10.5 1.5 15T499 165l8 4q34 15 54 36 37 38 37 97 0 35-16.5 67T537 420q-30 21-65 21-39 0-69-12-18-8-41-25l-5-4q-10-7-18-6-5 1-8 5-7 7-3 24 3 14 10 21 32 39 103 67l3 1q11 4 16 8 10 7 10 17v266q0 26 10 49t19.5 23 20-23 10.5-49V540q0-9 7.5-22.5T555 500q17-9 39-28 33-29 53-66 26-49 26-107 0-44-21-82-20-37-55-61t-75-30z',
+    blackMage:
+      'M783 146q-17 12-90 64-68 49-84 61-50 38-71 19-11-10-13-29-1-17 4.5-31t25.5-31l5-5q4-4 3.5-16t-6.5-19q-5-4-21-2t-36 11q-22 11-42 26l-11 9q-55 42-89 73-46 43-96 103-27 34-47 62-54 45-76.5 111t-7 134 65 117.5 118 65 134.5-7T559 785q31-22 64-49 58-48 100-94 31-33 72-87l10-12q15-20 26-43 8-20 10.5-36t-1.5-20q-7-6-19-7t-16 4l-5 5q-17 20-31 25.5t-31 3.5q-19-1-29-12-19-21 19-71 12-18 61-84 52-73 64-91 10-16 16-52 4-20 5-39l1-1-18 2q-21 2-38 6-24 6-36 13zM369 797q-50 0-92-28t-61.5-74.5-9.5-96 45.5-85.5 85.5-46 96.5 9.5T508 538t28 93q0 45-22.5 83T453 774.5 369 797zm305-419q-28 42-35 71-9 36 12 59 11 13 40 14 21 1 48-3l23-5q-10 16-24 30l-8 10q-26 34-47 55-27 27-73 65 11-59-7-116.5t-60.5-100-100-60.5-116.5-7q38-47 66-74 21-21 54-47l10-8q14-14 31-25l-5 24q-5 27-4 48 2 29 15 40 23 21 59 12 29-7 70-35 40-26 198-145l-1 1Q699 341 674 378z',
+    summoner:
+      'M776 347q-2-4-4-11-4-10-9-13-6-4-25.5-5.5T712 343q-3 16-2 51 1 14 0 19-1 15-18 21-10 4-34 5h-1q-9 1-34-12-15-8-53-30-45-27-67-38-27-13-81-54-31-24-91-73-46-37-56-43l-16-12q-19-14-26-16-11-3-19 9 44 103 76 200 26 81 42 157 12 60 18 113 3 39 3 70l-1 23-91 1q-23 0-27.5 7.5T237 768q3 5 30 13 31 9 69 15 49 6 95 4 57-2 105-17 57-17 99-51 83-83 121-167 29-65 30-129 1-36-8-82zM412 710q-4-102-20-194-19-110-50-175l25 23q31 28 56 48 36 28 57 36 9 4 36 13 61 21 89 34 48 22 55 43l-9 14q-26 42-46 65-34 40-75 63-53 28-118 30z',
+    scholar:
+      'M839 442q-6-9-15-18-36-11-49 17-6 13-7 34-1 18 2 24 13 30 13 48 0 33-24 57t-57.5 24-57.5-24-24-57q0-36 15-64 13-22 38-42 15-13 49-33 24-15 36-23l-4-3q-25-19-43-29-32-16-76-24-55-11-135-11t-135 11q-44 8-76 24-18 10-43 29l-4 3q12 8 36 23 34 20 49 33 25 20 38 42 15 28 15 64 0 34-24 57.5T298.5 628 241 604.5 217 547q0-18 13-48 3-6 2-24-1-21-7-34-13-28-49-17-9 9-15 18-15 22-25 49-11 30-11 56 0 61 38 109t98 61 114.5-14 80.5-82.5 11-115-63-96.5q23-9 49-13 17-3 47-4 30 1 47 4 26 4 49 13-49 37-63.5 96.5t11.5 115 80.5 82.5T739 716.5t98-61T875 547q0-26-11-56-10-27-25-49z',
+    ninja:
+      'M583 698q8-5 14-11 10-9 18-18 16-19 28-40 35-61 35-129 1-93-61-190-34-53-82-90-41-31-92-51-40-16-85-24-31-5-62-6l-25 1 17 2q20 4 43 12 31 11 60 28 37 22 70 51 39 34 70 78-8-3-17-6l-9-3q-12-3-26-5-18-2-38-2-24 0-47 4-28 5-54 15-29 13-56 32-30 22-55 53-28 34-51 78-46 88-39 187 4 71 35 141 15 35 29 56l-6-16q-7-20-11-43-6-32-6-66 0-43 9-86 10-51 33-100 1 9 3 18 4 20 12 38 4 9 8 17 32 64 91 102 82 54 206 48 62-2 119-26 47-19 90-53 33-27 62-62 21-25 37-51l12-21-11 12q-14 16-31 32-26 21-55 38-37 22-78 35-49 16-104 21zm-137-36q-39 0-71.5-19T323 591.5t-19-71 19-71.5 51.5-52 71-19 71 19 51.5 52 19 71.5-19 71-51.5 51.5-70.5 19z',
+    machinist:
+      'M756 479q-15-15-32-27-207-147-414-293l-1-1q-19-13-29-16-29-8-46 22-14 23 10 42 10 8 32 23l95 68q154 108 230 163l18 12q16 10 24 16 14 11 23 23-9 8-20 15-37 18-51 35t-10 38 27 54q14 20 31 38 14 14 11 28-3 12-18 24-13 10-18 12t-10-4q-4-4-16-23-23-34-33-53-16-33-45-17-22 13-7 43 24 48 37 71 22 39 45 66 18 22 30 22t30-23q13-17 37-52l11-16q22-32 26-42 6-15-1-29-5-10-30-40l-3-4q-10-12-26-30-20-21-22-28-3-9 10-16 10-6 51-21l5-1q11-5 34-16l3-2q27-14 27-30 1-13-15-31zm-205 59q6-26-.5-44T522 462q-40-27-119-83l-109-76q-11-8-22.5-8.5t-15 10.5 0 26 10.5 23 15 12q7 4 12 7 115 83 176 124l2 1q19 13 26 21 12 15 9 31-2 11-10 22t-17.5 13-18.5-4.5-8-16.5q1-26-10-39-8-11-32-20-8 21-10 46-1 14 1 43l1 10q2 23 14 31 11 6 34 2 34-5 52.5-14.5t29-28.5 18.5-56z',
+    darkKnight:
+      'M323 527q73-22 101-52 30-33 27-92-36 10-111 43-50 21-75 30-42 16-75 23 20-23 56-48 21-15 66-43 65-40 91-62-46-31-72-53-35-30-57-61 39-2 79 12 24 8 69 33l29 15q10-20 16-47 4-16 8.5-49.5T483 126q6-27 15-47 11 24 18 55 5 19 11 57 9 54 16 78 19-7 54-22 40-17 60-23 35-11 67-13-21 34-56 62-28 24-74 51 26 22 92 64 45 28 66 43 36 26 58 48-38-7-91-27-31-12-94-39t-88-35q-5 54 31.5 94T677 529q-13 18-44 56-37 43-52 66-26 38-33 71l-2 9q-14 63-22 95-14 52-29 95l-13-62q-19-90-30-135-9-38-41-86-19-28-64-82zm175-48q-12 6-23 15-6 6-16 18-9 10-14 15-15 15-4 32 10 14 32 49l19 29q32-36 45-57.5t12.5-36.5-14.5-31q-9-10-37-33z',
+    astrologian:
+      'M519 328q-115 0-229 1l-12-1q-12 0-17 1-7 2-10.5 10.5T247 367q-2 60 1 171 0 12 8.5 19.5t21 7 21-8T307 537q2-29 3-88v-1q0-28 4.5-40t13.5-15q5-1 21 0h3q48 3 96 2 17-1 25 3 11 4 16 17.5t5 41.5q0 21 1.5 63.5T497 584v50q1 42-1 53-3 15-17 18-9 3-46 2-33-2-55-2h-16q-31 1-39-1-11-3-14-13-1-7-1-33l1-33q0-2-5-6l-6-7q-125 78-160 212 54-55 94-88l7-6q11-12 15-13 8-1 3 22-4 21 20 28 14 4 46 4h9q42 1 127 0h32q33-1 47-5.5t19-18.5 6-45q1-94 3-280v-7q0-17-3-44-4-44-44-43zm166 22q-7-9-8-24-1-9 1-27 1-12 1-18-2-34-6-45-4-14-19-18-11-3-47-3l-144 1h-53q-16 1-24 9t-8 23q1 26 29 28 12 1 35 2l137 3q21 0 31 11t9 33q-1 51 0 154v126q0 12 3 18 5 8 16 9 28 4 36-20 4-15 4-31l1-19q1-39-2-58 4-48 13-72 11-28 39-47 19-12 59-34 48-26 74-44-50-11-99 2-46 12-78 41z',
+    samurai:
+      'M774 739q-64 73-155 102-87 29-180 11-37-7-65-20-52-23-24-51 7 4 43 9 28 4 62 6 64 4 125-18 64-24 113-72 29-32 35-73 7-46-22-84-17-23-45-33.5t-49-2.5q-15 6-24 20-5 9-7 19v-1q6 41 11 82 6 26-5 34-6 5-24 5H440q-18 0-24-5-11-8-5-34l4-34q3-33-2.5-48T393 531q-8-3-30-4t-34-3q-20-3-37-12-10-5-27-21.5T240 470q-12-5-18 6-8 13-11 52 5 79 48 133 14 11 17 21 2 5 0 14l-1 5q-1 5-4 10-6 7-16 5-14-3-35-24-50-55-69.5-133T148 403q17-81 68-139.5T338 173q69-31 144.5-32T628 170q23 10 38.5 24t11 21-26.5 6-43-9q-71-23-149-6-17 5-36 17-11 7-29 22-14 11-19 15-17 12-41 21-19 8-29 15l4-2q-8 5-15 11-4 4-10 12 6-7-6 6-16 25-10 54 6 26 28 47 36 27 69 32t56-9.5 30-44.5q25-97 50-111h1q28 16 54 130l3 10q2 11 5 15 4 7 13 7 12-3 27-6.5t23-4.5q11-1 32 1 23 2 51 16 22 11 42 26l1 1q10 11 16 11 11 0 14-32v1q-4-50-20-112-8-32-9-42 0-17 11-26 7-2 16.5 6.5T805 323q41 65 51 139.5t-11 147T774 739zM525 577q-11-137-24-150-13 13-24 150-1 10 2.5 16.5T491 600h21q7 0 10.5-6.5T525 577z',
+    redMage:
+      'M693 554q-5-2-11.5-6.5T669 542q-8-3-23-3-35 0-64 18.5T536.5 607 520 674q0 55 32 88.5t87 42.5q11 1 19.5 12t6.5 27-22 19q-15 1-38-5-68-13-107-60-42-50-42-129 0-49 22-91.5t62-70.5l2-1q25-14 28-51 7-130 21-275l3-27q3-33 5-43 3-16 10-16 6 0 10 15 2 11 5 44l2 27q11 105 19 227 3 33 12 59 8 24 15 30 10 4 29 15 17 10 24 13 12 5 19 4 6 1 4 7t-10.5 12.5-20.5 9-24-2.5zm-362-87q3-3 16 5 8 4 27 18l21 15q13 8 28 13l6 2q13 0 23 6.5t5 12.5q-33 40-42.5 102T435 765q16 32 43 64l1 2q0 8-11.5 5t-25-12-17.5-15q-11-9-23-26-7-6-17-1l-21 14q-19 14-27 18-13 8-16 5-3-4 5-17 4-8 19-28l14-20 4-7q3-6 3.5-15t-.5-15q-3-11-5-23v-1q-3-22-15-25l-8-2-27-5q-30-5-41-9-18-5-19-11 1-6 19-11 11-4 41-9l27-5 10-3q7-2 14.5-8.5T373 591q3-11 9-21 1-3 1-5-2-15-15-33l-14-21q-14-20-19-28-7-13-4-16zm240 204q0-19 11-35t29-23.5 37-4 32.5 17.5 17.5 33-3.5 37-23.5 29-36 11q-27 0-45.5-19T571 671z',
+    blueMage:
+      'M868 301q-2 0-13.5 2t-20.5 3q-2-33-29.5-63.5T745 206q-20-3-39 7-16 9-24 23.5t0 21.5q2 2 13 4 15 2 22 5 13 6 17 19 8 22 2 36.5T716 342l-97 29q-11 4-30 11-24 9-38 12-23 6-51 6t-51-6q-14-3-37-12-19-7-29-10l-32-10q-96-29-138-41-72-21-81-20-37 4-14 71l1 2q24 75 61 134 51 82 105 89 42 6 79-5 23-6 60-26 24-12 36-17 21-9 40-10 18 1 38 9 12 5 34 16-17 32-50 94-38 73-43 84l-7 15q-7 15-9 22-3 10 2.5 14t16.5-4q6-5 16-16l3-3q19-19 68-78 64-75 84-93 31 2 62-2 54-7 105-89 37-59 61-134l1-2q8-23 9-39 1-29-23-32zM290 521q-32-4-56-33-19-22-31-56-9-28-8-44 1-11 58 6 48 15 108 41.5t69 36-12.5 23T362 516q-39 9-72 5zm420 0q-33 4-72-5-34-8-55.5-21.5t-12.5-23 69-36T747 394q57-17 58-6 1 16-8 44-12 34-31 56-24 29-56 33z',
+    gunbreaker:
+      'M820 222q-34 48-68 95l-2 5q-4 7-13 18-12 14-32 34L366 710q46 29 99.5 33t103-17.5 83.5-63 45-94.5q4-23 18-33 11-8 24.5-7t23.5 11q13 12 10 31-14 90-67 150 0 5 4 19 7 24 7 36 1 20-10 29-10 8-25 2-9-3-28-14-12-7-16-9-8-4-11-2-77 43-164.5 37.5T305 760l-1 1q-68 49-93 65-13 9-21 11-7 1-10 0l-6-6q-2-4 0-10 2-9 10-21 20-28 68-95l3-5q4-7 12-18 13-15 33-35l306-303q-50-34-111-38t-115 24.5-85.5 81T263 525q0 17-7 30-8 14-25.5 15.5t-30.5-12-13-33.5q0-66 27-124t76-100 111-60q3-1 6-8 2-5 6-18 7-24 12-34 8-18 20-19t26 16q7 10 20 33 11 20 14 20 87 6 156 59 29-26 37-27l96-68q13-8 21-10 6-2 10 0l6 6q2 4 0 10-2 9-11 21z',
+    dancer:
+      'M801 424q-2 36-13 62.5T754 542q-15 19-58 65l-19 19q-54 59-91 108-21 29-32 49-7 13-17.5 39T520 858q-10 14-22 14t-22-14q-5-10-16-36t-18-39q-34-61-123-157l-19-19q-43-46-58-65-23-29-34-55.5T195 424q-4-53 22-90 23-34 60-43 50-12 96-43 74-50 125-143 51 93 125 143 46 31 96 43 37 9 60 43 26 37 22 90zm-65-15q-3-31-20-49-15-15-37.5-18t-44.5 5q-30 9-37 43-4 20 0 61 2 22 2 29-1 12-7 13-15 3-32-16-16-16-28-41.5T516 392q-1-6 1-17 2-15 8-28 6-16 28-33 19-14 28-15-10-3-47-41l-36-38h-1q-67 74-82 79 9 1 28 15 22 17 28 33 6 13 8 28 2 11 1 17-3 18-15.5 43.5T437 477q-18 19-33 16-6-1-7-13 0-7 2-29 4-41 0-61-7-34-37-43-22-8-44.5-5T280 360q-17 18-20 49-4 38 21 79 15 24 55 68l16 17q33 37 87 85 28 26 38 37 16 17 21 32 5-15 21-32 10-11 38-37 54-48 87-85l16-17q40-43 55-68 25-41 21-79z',
+    reaper:
+      'M846 560q-2-4-11 0-6 2-21 12-76 48-180 79-107 31-182 25-58-4-98-19l47-150q22 54 72 83 40 23 85 22.5t84-22.5 62-62q22-37 23-79t-17-79.5-52-62.5q-1-1-1-17 0-18-2-26-2-15-9-18t-20 5q-7 5-20 18-10 9-11 9-34-8-67.5-1.5T465 301l36-116q6-16 9-39 4-33-6-36.5T477 135q-11 20-16 36L299 622q-17-17-24-39t-5-51q1-18 7-51l1-5q5-26-.5-28.5t-20 20T232 516q-15 33-19 61-5 41 7 77 13 41 48 67l-18 78q-5 15-7 33-2 27 8.5 30.5T276 843q10-15 15-30l30-66q17 5 32 8 63 10 139-2 83-12 164-50 89-41 164-108 16-14 21-21 8-10 5-14zM466 390q15-24 39-38t52.5-14.5 53 14 38.5 39 14 52.5-14 53-38.5 38.5-52.5 14-53-14-39-39-14-52.5 14-53z',
+    sage: 'M872 223q-18 33-56 96-49 82-63 112-4 10-4 28 1 11 0 18-2 31-10.5 47.5T721 531l-10-11q-26-31-40-42-24-17-56-23-21-3-20-13.5t17.5-23T651 397q4-2 10-4 13-3 25-10 15-12 36-36 12-15 39-49 52-66 80-93 16-11 25.5-6t5.5 24zM563 373q-18 0-31.5-9.5t-13.5-23 13.5-23 32-9.5 32 9.5 13.5 23-13.5 23T563 373zm-135 87q-26 0-45-13.5T364 414t19-32.5 45.5-13.5 45.5 13.5 19 32.5-19 32.5-46 13.5zm41-192q-64 31-116 79-58 53-84 110-29 65-13 126 6 20 17 38 7 11 21 27 16 19 21 30 7 17 2 41-14 36-42 35-70 0-105-111-25-79 5-159 28-78 100-140t171-91q12-2 21.5-.5t10 6-8.5 9.5zM364 584q-25 0-42-12.5T305 542t17-29.5 41.5-12.5 42 12.5T423 542t-17.5 29.5T364 584zm58 111q6-17 15-47 13-43 21-62 14-31 36-54 25-28 69-53 27-6 58 6 28 11 50.5 34t31.5 48q-7 85-64 142-53 52-132.5 68T348 768q1-12 11-22 6-5 22.5-14t24.5-14q12-10 16-23zm60 22q18 5 49-1 28-8 55-27 30-22 45-49 17-32 7-63-5-12-17.5-23T592 539q-19-4-29 5-83 71-81 173z',
+    viper:
+      'M680 161q2 42-2 63.5T659 268q-13 18-22.5 25t-22.5 9q-8 1-30 1h-12q-17 53-22 87-4 31 2 56 3 16 15 48 18 44 26 79 27 69-3 274 27-22 45-57 16-30 27-74 7-26 18-80 9-46 14-67 9-36 19-61 8-14 33 10 29-111 12-195-16-80-78-162zm15 140q13 49 15 74 2 35-8 69-25 0-38 17-11 15-15 44-3 17-3 57l-1 23-3 8q-4 12-6 15-3 5-3-5 2-44-4-78-4-20-14.5-49T602 430q-2-28 8-67 11 0 29-8t32-21q18-15 24-33zM316 161q-2 42 2.5 63.5T337 268q13 18 23 25t22 9q9 1 31 1h12q17 53 21 87 4 31-1 56-3 16-16 48-17 44-26 79-27 69 4 274-27-22-46-57-15-30-27-74-7-26-18-80-8-46-14-67-8-36-19-61-8-14-33 10-28-111-12-195 16-80 78-162zm-15 140q-12 49-14 74-3 35 7 69 26 0 39 17 11 15 14 44 3 17 4 57v23l3 8q5 12 6 15 3 5 3-5-2-44 5-78 3-20 13.5-49t12.5-46q3-28-7-67-12 0-29.5-8T325 334q-17-15-24-33z',
+    pictomancer:
+      'M872 223q-18 33-56 96-49 82-63 112-4 10-4 28 1 11 0 18-2 31-10.5 47.5T721 531l-10-11q-26-31-40-42-24-17-56-23-21-3-20-13.5t17.5-23T651 397q4-2 10-4 13-3 25-10 15-12 36-36 12-15 39-49 52-66 80-93 16-11 25.5-6t5.5 24zM563 373q-18 0-31.5-9.5t-13.5-23 13.5-23 32-9.5 32 9.5 13.5 23-13.5 23T563 373zm-135 87q-26 0-45-13.5T364 414t19-32.5 45.5-13.5 45.5 13.5 19 32.5-19 32.5-46 13.5zm41-192q-64 31-116 79-58 53-84 110-29 65-13 126 6 20 17 38 7 11 21 27 16 19 21 30 7 17 2 41-14 36-42 35-70 0-105-111-25-79 5-159 28-78 100-140t171-91q12-2 21.5-.5t10 6-8.5 9.5zM364 584q-25 0-42-12.5T305 542t17-29.5 41.5-12.5 42 12.5T423 542t-17.5 29.5T364 584zm58 111q6-17 15-47 13-43 21-62 14-31 36-54 25-28 69-53 27-6 58 6 28 11 50.5 34t31.5 48q-7 85-64 142-53 52-132.5 68T348 768q1-12 11-22 6-5 22.5-14t24.5-14q12-10 16-23zm60 22q18 5 49-1 28-8 55-27 30-22 45-49 17-32 7-63-5-12-17.5-23T592 539q-19-4-29 5-83 71-81 173z',
+  },
+};
+
+const ffxivJobIconPaths = Object.values(ffxivJobIconData.paths);
diff --git a/playgrounds/ui-craft-playground/app/page.tsx b/playgrounds/ui-craft-playground/app/page.tsx
index 8d5f54e..6470217 100644
--- a/playgrounds/ui-craft-playground/app/page.tsx
+++ b/playgrounds/ui-craft-playground/app/page.tsx
@@ -1,10 +1,9 @@
-import { FfxivJobIconsLoopUsingGooeyEffect } from '@siluat/ui-craft/react/ffxiv-job-icons-loop-using-gooey-effect';
-
+import { FfxivJobIconsLoopUsingFlubber } from '@siluat/ui-craft/react/ffxiv-job-icons-loop-using-flubber';
 export default function Page() {
   return (
     <main className="flex flex-col items-center justify-center h-screen">
-      <span>FFXIV Job Icons Loop Using Gooey Effect</span>
-      <FfxivJobIconsLoopUsingGooeyEffect />
+      <span>FFXIV Job Icons Loop using Flubber</span>
+      <FfxivJobIconsLoopUsingFlubber />
     </main>
   );
 }
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a4a4c4c..e3445d4 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -67,6 +67,13 @@ importers:
   packages/typescript-config: {}
 
   packages/ui-craft:
+    dependencies:
+      flubber:
+        specifier: ^0.4.2
+        version: 0.4.2
+      framer-motion:
+        specifier: ^12.5.0
+        version: 12.5.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
     devDependencies:
       '@siluat/typescript-config':
         specifier: workspace:*
@@ -74,6 +81,9 @@ importers:
       '@testing-library/react':
         specifier: ^16.2.0
         version: 16.2.0(@testing-library/dom@10.4.0)(@types/react-dom@19.0.4(@types/react@19.0.11))(@types/react@19.0.11)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+      '@types/flubber':
+        specifier: ^0.4.0
+        version: 0.4.0
       '@types/react':
         specifier: ^19.0.11
         version: 19.0.11
@@ -1430,6 +1440,9 @@ packages:
   '@types/estree@1.0.6':
     resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==}
 
+  '@types/flubber@0.4.0':
+    resolution: {integrity: sha512-q9u6zseKgOBc0sAn/aPyi0vMNtXiLrNHSvj/tMXg+X2vk0jwX7obXgLISBmryGFvfszbId1JGT/8CcwB3vC6yg==}
+
   '@types/hast@3.0.4':
     resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
 
@@ -1766,6 +1779,9 @@ packages:
   comma-separated-tokens@2.0.3:
     resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
 
+  commander@2.20.3:
+    resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
+
   common-ancestor-path@1.0.1:
     resolution: {integrity: sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==}
 
@@ -1807,6 +1823,12 @@ packages:
   csstype@3.1.3:
     resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
 
+  d3-array@1.2.4:
+    resolution: {integrity: sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==}
+
+  d3-polygon@1.0.6:
+    resolution: {integrity: sha512-k+RF7WvI08PC8reEoXa/w2nSg5AUMTi+peBD9cmFc+0ixHfbs4QmxxkarVal1IkVkgxVuk9JSHhJURHiyHKAuQ==}
+
   dataloader@1.4.0:
     resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==}
 
@@ -1888,6 +1910,9 @@ packages:
     resolution: {integrity: sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==}
     engines: {node: '>=4'}
 
+  earcut@2.2.4:
+    resolution: {integrity: sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==}
+
   electron-to-chromium@1.5.118:
     resolution: {integrity: sha512-yNDUus0iultYyVoEFLnQeei7LOQkL8wg8GQpkPCRrOlJXlcCwa6eGKZkxQ9ciHsqZyYbj8Jd94X1CTPzGm+uIA==}
 
@@ -2020,6 +2045,23 @@ packages:
     resolution: {integrity: sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==}
     engines: {node: '>=8'}
 
+  flubber@0.4.2:
+    resolution: {integrity: sha512-79RkJe3rA4nvRCVc2uXjj7U/BAUq84TS3KHn6c0Hr9K64vhj83ZNLUziNx4pJoBumSPhOl5VjH+Z0uhi+eE8Uw==}
+
+  framer-motion@12.5.0:
+    resolution: {integrity: sha512-buPlioFbH9/W7rDzYh1C09AuZHAk2D1xTA1BlounJ2Rb9aRg84OXexP0GLd+R83v0khURdMX7b5MKnGTaSg5iA==}
+    peerDependencies:
+      '@emotion/is-prop-valid': '*'
+      react: ^18.0.0 || ^19.0.0
+      react-dom: ^18.0.0 || ^19.0.0
+    peerDependenciesMeta:
+      '@emotion/is-prop-valid':
+        optional: true
+      react:
+        optional: true
+      react-dom:
+        optional: true
+
   fs-extra@11.2.0:
     resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==}
     engines: {node: '>=14.14'}
@@ -2687,6 +2729,12 @@ packages:
   mlly@1.7.4:
     resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==}
 
+  motion-dom@12.5.0:
+    resolution: {integrity: sha512-uH2PETDh7m+Hjd1UQQ56yHqwn83SAwNjimNPE/kC+Kds0t4Yh7+29rfo5wezVFpPOv57U4IuWved5d1x0kNhbQ==}
+
+  motion-utils@12.5.0:
+    resolution: {integrity: sha512-+hFFzvimn0sBMP9iPxBa9OtRX35ZQ3py0UHnb8U29VD+d8lQ8zH3dTygJWqK7av2v6yhg7scj9iZuvTS0f4+SA==}
+
   mri@1.2.0:
     resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
     engines: {node: '>=4'}
@@ -3300,6 +3348,12 @@ packages:
     resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
     engines: {node: '>=8'}
 
+  svg-path-properties@0.2.2:
+    resolution: {integrity: sha512-GmrB+b6woz6CCdQe6w1GHs/1lt25l7SR5hmhF8jRdarpv/OgjLyuQygLu1makJapixeb1aQhP/Oa1iKi93o/aQ==}
+
+  svgpath@2.6.0:
+    resolution: {integrity: sha512-OIWR6bKzXvdXYyO4DK/UWa1VA1JeKq8E+0ug2DG98Y/vOmMpfZNj+TIG988HjfYSqtcy/hFOtZq/n/j5GSESNg==}
+
   table@6.9.0:
     resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==}
     engines: {node: '>=10.0.0'}
@@ -3352,6 +3406,10 @@ packages:
     resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
     engines: {node: '>=8.0'}
 
+  topojson-client@3.1.0:
+    resolution: {integrity: sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==}
+    hasBin: true
+
   tough-cookie@4.1.4:
     resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==}
     engines: {node: '>=6'}
@@ -5205,6 +5263,8 @@ snapshots:
 
   '@types/estree@1.0.6': {}
 
+  '@types/flubber@0.4.0': {}
+
   '@types/hast@3.0.4':
     dependencies:
       '@types/unist': 3.0.3
@@ -5642,6 +5702,8 @@ snapshots:
 
   comma-separated-tokens@2.0.3: {}
 
+  commander@2.20.3: {}
+
   common-ancestor-path@1.0.1: {}
 
   confbox@0.1.8: {}
@@ -5674,6 +5736,10 @@ snapshots:
 
   csstype@3.1.3: {}
 
+  d3-array@1.2.4: {}
+
+  d3-polygon@1.0.6: {}
+
   dataloader@1.4.0: {}
 
   debug@4.4.0:
@@ -5726,6 +5792,8 @@ snapshots:
 
   dset@3.1.4: {}
 
+  earcut@2.2.4: {}
+
   electron-to-chromium@1.5.118: {}
 
   electron-to-chromium@1.5.120: {}
@@ -5880,6 +5948,24 @@ snapshots:
 
   flattie@1.1.1: {}
 
+  flubber@0.4.2:
+    dependencies:
+      d3-array: 1.2.4
+      d3-polygon: 1.0.6
+      earcut: 2.2.4
+      svg-path-properties: 0.2.2
+      svgpath: 2.6.0
+      topojson-client: 3.1.0
+
+  framer-motion@12.5.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
+    dependencies:
+      motion-dom: 12.5.0
+      motion-utils: 12.5.0
+      tslib: 2.8.1
+    optionalDependencies:
+      react: 19.0.0
+      react-dom: 19.0.0(react@19.0.0)
+
   fs-extra@11.2.0:
     dependencies:
       graceful-fs: 4.2.11
@@ -6828,6 +6914,12 @@ snapshots:
       pkg-types: 1.3.1
       ufo: 1.5.4
 
+  motion-dom@12.5.0:
+    dependencies:
+      motion-utils: 12.5.0
+
+  motion-utils@12.5.0: {}
+
   mri@1.2.0: {}
 
   mrmime@2.0.1: {}
@@ -7514,6 +7606,10 @@ snapshots:
     dependencies:
       has-flag: 4.0.0
 
+  svg-path-properties@0.2.2: {}
+
+  svgpath@2.6.0: {}
+
   table@6.9.0:
     dependencies:
       ajv: 8.17.1
@@ -7555,6 +7651,10 @@ snapshots:
     dependencies:
       is-number: 7.0.0
 
+  topojson-client@3.1.0:
+    dependencies:
+      commander: 2.20.3
+
   tough-cookie@4.1.4:
     dependencies:
       psl: 1.15.0


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: ASSERTIVE**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between fd61aa2abf0867aabe97a238ae2ba10dfcd1fee9 and 9382fc44c83dfd46f76b7ad5d6d012a21f071283.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `pnpm-lock.yaml` is excluded by `!**/pnpm-lock.yaml`

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `packages/ui-craft/package.json` (2 hunks)
* `packages/ui-craft/react/ffxiv-job-icons-loop-using-flubber.test.tsx` (1 hunks)
* `packages/ui-craft/react/ffxiv-job-icons-loop-using-flubber.tsx` (1 hunks)
* `playgrounds/ui-craft-playground/app/page.tsx` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Definitions (2)</summary>

<details>
<summary>packages/ui-craft/react/ffxiv-job-icons-loop-using-flubber.test.tsx (1)</summary><blockquote>

<details>
<summary>packages/ui-craft/react/ffxiv-job-icons-loop-using-flubber.tsx (1)</summary>

* `useIncrementingTuple` (57-75)

</details>

</blockquote></details>
<details>
<summary>playgrounds/ui-craft-playground/app/page.tsx (1)</summary><blockquote>

<details>
<summary>packages/ui-craft/react/ffxiv-job-icons-loop-using-flubber.tsx (1)</summary>

* `FfxivJobIconsLoopUsingFlubber` (7-17)

</details>

</blockquote></details>

</details>

</details>

<details>
<summary>🔇 Additional comments (12)</summary><blockquote>

<details>
<summary>playgrounds/ui-craft-playground/app/page.tsx (2)</summary>

`1-1`: **코드 변경사항이 적절해 보입니다.**

`FfxivJobIconsLoopUsingGooeyEffect` 대신 새로 구현된 `FfxivJobIconsLoopUsingFlubber` 컴포넌트를 올바르게 임포트하고 있습니다.

---

`5-6`: **텍스트와 컴포넌트가 일관되게 변경되었습니다.**

화면에 표시되는 텍스트와 렌더링되는 컴포넌트가 모두 적절하게 Flubber를 사용하는 버전으로 업데이트되었습니다.

</details>
<details>
<summary>packages/ui-craft/package.json (2)</summary>

`17-17`: **타입 정의 추가가 적절합니다.**

Flubber 라이브러리를 위한 타입 정의를 개발 의존성으로 올바르게 추가했습니다.

---

`30-33`: **새로운 의존성 추가가 적절합니다.**

애니메이션을 위한 `flubber`와 `framer-motion` 라이브러리를 의존성으로 적절하게 추가했습니다.

</details>
<details>
<summary>packages/ui-craft/react/ffxiv-job-icons-loop-using-flubber.test.tsx (3)</summary>

`1-13`: **size <= 1 테스트 케이스가 잘 구현되었습니다.**

`useIncrementingTuple` 훅의 첫 번째 테스트 케이스로, 사이즈가 1 이하인 경우 항상 [0, 0]을 반환하는지 확인하는 테스트가 올바르게 구현되었습니다.

---

`15-29`: **size = 2 테스트 케이스가 잘 구현되었습니다.**

사이즈가 2인 경우 증가 함수를 호출할 때마다 [0, 1]과 [1, 0] 사이를 토글하는지 검증하는 테스트가 올바르게 구현되었습니다.

---

`31-48`: **size > 2 테스트 케이스가 잘 구현되었습니다.**

사이즈가 2보다 큰 경우(테스트에서는 3)에 증가 함수가 튜플을 올바르게 업데이트하는지 확인하는 테스트가 적절하게 구현되었습니다. 예상되는 시퀀스 [0,1] -> [1,2] -> [2,0] -> [0,1]을 정확히 검증합니다.

</details>
<details>
<summary>packages/ui-craft/react/ffxiv-job-icons-loop-using-flubber.tsx (5)</summary>

`1-6`: **필요한 패키지 임포트가 잘 구현되었습니다.**

클라이언트 컴포넌트임을 명시하고, flubber와 framer-motion 라이브러리, 그리고 필요한 React 훅들을 적절하게 임포트하고 있습니다.

---

`7-17`: **FfxivJobIconsLoopUsingFlubber 컴포넌트가 잘 구현되었습니다.**

컴포넌트가 적절히 SVG를 사용하여 아이콘을 표시하고, 훅을 통해 경로 모핑을 적용하고 있습니다. 뷰박스와 사이즈 설정이 올바르며, 접근성을 위한 title 요소도 포함되어 있습니다.

---

`57-75`: **useIncrementingTuple 훅이 잘 구현되었습니다.**

사이즈에 따라 튜플 값을 초기화하고, `useCallback`을 사용하여 증가 함수를 최적화했습니다. 사이즈가 1 이하인 경우에 대한 예외 처리도 적절합니다.

---

`77-123`: **FFXIV 직업 아이콘 데이터가 잘 정의되었습니다.**

각 직업별 아이콘 경로와 뷰박스 정보가 잘 정의되어 있습니다. 각 경로는 SVG 경로 문자열로 적절하게 구성되어 있습니다.

---

`124-124`: **아이콘 경로 배열이 효율적으로 생성되었습니다.**

Object.values를 사용하여 모든 아이콘 경로를 배열로 변환하는 방식이 간결하고 효율적입니다.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: CHANGES_REQUESTED
author_association: NONE
submitted_at: 2025-03-25T12:10:54Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 4**

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: ASSERTIVE**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 9382fc44c83dfd46f76b7ad5d6d012a21f071283 and fbe66c69393565f8da09cb6f9ab0983a1bad636a.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `packages/ui-craft/react/ffxiv-job-icons-loop-using-flubber.tsx` (1 hunks)

</details>

<details>
<summary>🔇 Additional comments (5)</summary><blockquote>

<details>
<summary>packages/ui-craft/react/ffxiv-job-icons-loop-using-flubber.tsx (5)</summary>

`1-5`: **'use client' 및 import 구문이 적절합니다**  
Next.js 클라이언트 컴포넌트의 요구사항에 맞게 잘 선언되어 있어 별다른 문제가 없습니다.

---

`19-22`: **pathList 최소 길이 검사 구현이 올바릅니다**  
`pathList.length < 2`인 경우 예외를 던지면서 부적절한 인자를 미리 차단해, 잠재적 오류를 방지하고 있습니다.

---

`24-33`: **useMemo + flubber.interpolate 결합이 효율적입니다**  
`from`이나 `to`가 바뀔 때만 interpolate 함수를 재생성하므로 불필요한 계산을 줄일 수 있습니다. 애니메이션 성능에 이점이 있습니다.

---

`52-53`: **currentPath 반환 로직이 단순 명확합니다**  
상태(`currentPath`)를 직접 변경하여 SVG 경로를 즉각적으로 반영하는 구조가 깔끔합니다. 추가 수정 없이도 이해하기 쉽습니다.

---

`55-73`: **useIncrementingTuple 훅의 예외처리가 안전합니다**  
`size <= 1`인 상황을 미리 방지하여, 불필요한 증가 로직 수행을 막고 잠재 오류를 줄였습니다.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: CHANGES_REQUESTED
author_association: NONE
submitted_at: 2025-03-25T12:18:24Z
user: login: coderabbitai[bot]
state: APPROVED
author_association: NONE
submitted_at: 2025-03-25T12:21:17Z

Review comments:
diff_hunk: @@ -0,0 +1,124 @@
+'use client';
+
+import { interpolate } from 'flubber';
+import { animate } from 'framer-motion';
+import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
+
+export function FfxivJobIconsLoopUsingFlubber() {
+  const path = useSvgPathMorphing(ffxivJobIconPaths);
+  return (
+    <svg viewBox={ffxivJobIconData.viewBox} width={80} height={80}>
+      <title>FFXIV Job Icons Loop</title>
+      <g fill="currentColor">
+        <path d={path} />
+      </g>
+    </svg>
+  );
+}
+
+function useSvgPathMorphing(pathList: string[]) {
+  if (pathList.length < 2) {
+    throw new Error(
+      'useSvgPathMorphing: pathList length must be greater than 2',
+    );
+  }
+
+  const [[from, to], increment] = useIncrementingTuple(pathList.length);
+
+  const interpolater = useMemo(() => {
+    const fromPath = pathList[from];
+    const toPath = pathList[to];
+    if (!fromPath || !toPath) {
+      throw new Error('Invalid path index');
+    }
+    return interpolate(fromPath, toPath);
+  }, [from, to, pathList]);
+
+  const [currentPath, setCurrentPath] = useState(pathList[from]);
+
+  useLayoutEffect(() => {
+    const animation = animate<number>(0, 1, {
+      duration: 0.2,
+      delay: 0.3,
+      ease: 'easeIn',
+      onUpdate: (progress) => {
+        setCurrentPath(interpolater(progress));
+      },
+      onComplete: () => {
+        increment();
+      },
+    });
+    return () => animation.stop();
+  }, [increment, interpolater]);
+
+  return currentPath;
+}
path: packages/ui-craft/react/ffxiv-job-icons-loop-using-flubber.tsx
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**useSvgPathMorphing 훅이 잘 구현되었습니다.**

경로 목록의 유효성을 검사하고, `useIncrementingTuple`을 활용하여 경로 간 전환을 관리하며, flubber의 interpolate 함수를 이용해 애니메이션을 구현하고 있습니다. 애니메이션 완료 시 다음 경로로 이동하는 로직도 적절합니다.

한 가지 소소한 개선점은 에러 메시지에서 'greater than 2'를 'at least 2'로 변경하면 좀 더 명확할 것 같습니다.



```diff
-      'useSvgPathMorphing: pathList length must be greater than 2',
+      'useSvgPathMorphing: pathList length must be at least 2',
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
function useSvgPathMorphing(pathList: string[]) {
  if (pathList.length < 2) {
    throw new Error(
      'useSvgPathMorphing: pathList length must be at least 2',
    );
  }

  const [[from, to], increment] = useIncrementingTuple(pathList.length);

  const interpolater = useMemo(() => {
    const fromPath = pathList[from];
    const toPath = pathList[to];
    if (!fromPath || !toPath) {
      throw new Error('Invalid path index');
    }
    return interpolate(fromPath, toPath);
  }, [from, to, pathList]);

  const [currentPath, setCurrentPath] = useState(pathList[from]);

  useLayoutEffect(() => {
    const animation = animate<number>(0, 1, {
      duration: 0.2,
      delay: 0.3,
      ease: 'easeIn',
      onUpdate: (progress) => {
        setCurrentPath(interpolater(progress));
      },
      onComplete: () => {
        increment();
      },
    });
    return () => animation.stop();
  }, [increment, interpolater]);

  return currentPath;
}
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit fbe66c6
created_at: 2025-03-25T12:10:53Z
author_association: NONE
diff_hunk: @@ -0,0 +1,122 @@
+'use client';
+
+import { interpolate } from 'flubber';
+import { animate } from 'framer-motion';
+import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
+
+export function FfxivJobIconsLoopUsingFlubber() {
+  const path = useSvgPathMorphing(ffxivJobIconPaths);
+  return (
+    <svg viewBox={ffxivJobIconData.viewBox} width={80} height={80}>
+      <title>FFXIV Job Icons Loop</title>
+      <g fill="currentColor">
+        <path d={path} />
+      </g>
+    </svg>
+  );
+}
+
+function useSvgPathMorphing(pathList: string[]) {
+  if (pathList.length < 2) {
+    throw new Error('useSvgPathMorphing: pathList length must be at least 2');
+  }
+
+  const [[from, to], increment] = useIncrementingTuple(pathList.length);
+
+  const interpolater = useMemo(() => {
+    const fromPath = pathList[from];
+    const toPath = pathList[to];
+    if (!fromPath || !toPath) {
+      throw new Error('Invalid path index');
+    }
+    return interpolate(fromPath, toPath);
+  }, [from, to, pathList]);
+
+  const [currentPath, setCurrentPath] = useState(pathList[from]);
+
+  useLayoutEffect(() => {
+    const animation = animate<number>(0, 1, {
+      duration: 0.2,
+      delay: 0.3,
+      ease: 'easeIn',
+      onUpdate: (progress) => {
+        setCurrentPath(interpolater(progress));
+      },
+      onComplete: () => {
+        increment();
+      },
+    });
+    return () => animation.stop();
+  }, [increment, interpolater]);
path: packages/ui-craft/react/ffxiv-job-icons-loop-using-flubber.tsx
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**애니메이션 인자(duration·delay)를 상수나 prop으로 추출해보세요**  
하드코딩된 값(0.2, 0.3)은 재사용성과 가독성을 떨어뜨릴 수 있으므로, 상수나 컴포넌트 prop으로 분리하여 관리하는 방안을 고려해보세요.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-25T12:18:23Z
author_association: NONE
diff_hunk: @@ -0,0 +1,122 @@
+'use client';
+
+import { interpolate } from 'flubber';
+import { animate } from 'framer-motion';
+import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
+
+export function FfxivJobIconsLoopUsingFlubber() {
+  const path = useSvgPathMorphing(ffxivJobIconPaths);
+  return (
+    <svg viewBox={ffxivJobIconData.viewBox} width={80} height={80}>
+      <title>FFXIV Job Icons Loop</title>
+      <g fill="currentColor">
+        <path d={path} />
+      </g>
+    </svg>
+  );
+}
path: packages/ui-craft/react/ffxiv-job-icons-loop-using-flubber.tsx
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**SVG 크기 설정을 유연하게 개선해보세요**  
`width`와 `height`가 고정 값으로 선언되어 있어, 상황에 따라 사용자화가 필요할 수 있습니다. props나 context를 통해 크기를 조정 가능하게 만들면 활용도가 높아질 것입니다.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-25T12:18:23Z
author_association: NONE
diff_hunk: @@ -0,0 +1,122 @@
+'use client';
+
+import { interpolate } from 'flubber';
+import { animate } from 'framer-motion';
+import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
+
+export function FfxivJobIconsLoopUsingFlubber() {
+  const path = useSvgPathMorphing(ffxivJobIconPaths);
+  return (
+    <svg viewBox={ffxivJobIconData.viewBox} width={80} height={80}>
+      <title>FFXIV Job Icons Loop</title>
+      <g fill="currentColor">
+        <path d={path} />
+      </g>
+    </svg>
+  );
+}
+
+function useSvgPathMorphing(pathList: string[]) {
+  if (pathList.length < 2) {
+    throw new Error('useSvgPathMorphing: pathList length must be at least 2');
+  }
+
+  const [[from, to], increment] = useIncrementingTuple(pathList.length);
+
+  const interpolater = useMemo(() => {
+    const fromPath = pathList[from];
+    const toPath = pathList[to];
+    if (!fromPath || !toPath) {
+      throw new Error('Invalid path index');
+    }
+    return interpolate(fromPath, toPath);
+  }, [from, to, pathList]);
+
+  const [currentPath, setCurrentPath] = useState(pathList[from]);
+
+  useLayoutEffect(() => {
+    const animation = animate<number>(0, 1, {
+      duration: 0.2,
+      delay: 0.3,
+      ease: 'easeIn',
+      onUpdate: (progress) => {
+        setCurrentPath(interpolater(progress));
+      },
+      onComplete: () => {
+        increment();
+      },
+    });
+    return () => animation.stop();
+  }, [increment, interpolater]);
+
+  return currentPath;
+}
+
+export function useIncrementingTuple(
+  size: number,
+): [[number, number], () => void] {
+  const [tuple, setTuple] = useState<[number, number]>(
+    size > 1 ? [0, 1] : [0, 0],
+  );
+
+  const increment = useCallback(() => {
+    if (size <= 1) return;
+
+    setTuple(([a, b]) => {
+      const nextA = (a + 1) % size;
+      const nextB = (b + 1) % size;
+      return [nextA, nextB];
+    });
+  }, [size]);
+
+  return [tuple, increment];
+}
+
+const ffxivJobIconData = {
+  viewBox: '0 0 1024 1024',
+  paths: {
+    paladin:
+      'M757 139q-5 0-14 10-6 6-18.5 21.5T707 192q-9 10-14 10H555q-4 0-20-34l-6-13q-6-12-14-20-5-6-11-8l-4-2-4 2q-6 2-11 8-8 8-14 20l-6 13q-16 34-20 34H307q-5 0-14-10-5-6-17.5-21.5T257 149q-9-10-14-10-14 0-26 13-14 14-14 35v258q0 96 33 177 31 76 88 130 48 45 115 88 34 21 61 35 26-13 61-35 67-43 115-88 57-54 88-130 33-81 33-177V187q0-21-14-35-12-13-26-13zM469 768q-11-6-21-12-26-17-49-37-34-29-59-61-31-40-47-94-12-42-12-80l1-219h187v503zm191-110q-25 32-59 61-23 20-49 37l-21 12V265h187l1 219q0 38-12 80-16 54-47 94z',
+    monk: 'M525 235q21-18 36-39 12-17 19-35 6-13 9-26l1-10h-10q-12 1-26 4-20 4-38 13-24 11-45 29L190 407q-20 17-35 38-12 17-20 35-6 13-8 26l-2 10h10q13-1 27-4 19-4 38-13 23-11 44-29l140-118zm142 169q21-17 36-38 12-17 20-36 5-13 8-25l2-10h-10q-13 1-27 4-19 4-38 13-23 11-44 29L333 576q-21 18-36 39-12 17-20 35-5 13-8 26l-2 9h10q13 0 27-3 19-5 38-13 23-11 44-29l141-118zm89 106L616 628 475 746q-21 18-36 39-12 16-19 35-6 13-9 25l-1 10h10q12 0 26-3 20-5 38-14 24-11 45-28l281-236q38-32 55-74 9-21 10-35h-10q-13 0-27 3-19 5-38 13-23 12-44 29z',
+    warrior:
+      'M875 500l-2-26q-3-31-12-63-11-44-31-82-17-31-45.5-62t-57-49-43-14-22.5 12q-11 11-16 32l-4 22q-4 21-9 32-7 17-25 38-22 25-67 33-23 3-41 2-20 1-43-2-44-8-65-33-18-21-25-38-5-11-9-32l-4-22q-5-21-16-31-8-9-22.5-13t-43 14-57 49-45.5 62q-20 38-31 82-9 32-12 64l-2 25q1 24 7 57 11 63 39 114 16 31 45 62t57 49 42.5 14 23.5-12q10-11 15-32l5-22q4-21 8-32 8-17 25-38 16-18 44-27 20-7 44-8h20q21-2 43 2 44 8 65 33 18 21 25 38 5 11 9 32l4 22q5 21 16 31 8 9 22.5 13t43-14.5 57-49T830 670q20-37 31-81 9-32 12-64zM760 627q-9 18-22 35-9 12-18 22l-8 7-3-11q-5-15-12-29-10-19-23-35-16-19-36-32-24-16-51-25-22-7-44-11-16-2-31-2l-12 1q-18-2-45 2-50 8-92 35-37 23-59 68-12 22-16 40l-7-8q-10-10-19-22-12-17-22-35-13-26-20-58-5-24-6-49l-1-20q-1-19 2-44 6-48 25-83 9-18 22-35 9-12 18-22l8-7 3 11q5 15 12 28 10 20 23 36 16 19 36 32 44 28 95 35 26 4 43 2 19 2 44-2 51-8 94-35 20-13 36-33 13-15 23-35 7-14 12-28l3-12 8 8q9 10 18 22 13 17 22 35 19 35 25 82 3 25 2 45v1q1 22-3 46-6 46-24 80z',
+    dragoon:
+      'M723 335h-38l-8 10q-9 12-18 19-11 10-17 10-11 0-18-14t-7-28V171q0-13-7-30-4-13-12-25l-7-11-7 12q-7 13-12 26-7 17-7 28v172l-5 7q-7 8-19 16-19 13-41 16-22-3-41-16t-24-23V171q0-11-7-28-5-13-12-26l-7-12-7 11q-8 12-12 25-7 17-7 30v161q0 14-7 28t-18 14q-6 0-17-10-9-7-18-19l-8-10h-38q-6 0-10.5 4t-3.5 10q0 4 72 165 53 117 58 130 7 19 41 122l19 56q5 13 14 23t16 10l17 1 17-1q7 0 16-10t14-23l19-56q34-103 41-122 5-13 58-130 72-161 72-165 0-6-4-10t-10-4zm-92 142q-5 14-24 35-17 20-34 31l-10 6q-12 7-19.5 12.5t-14 14T523 588t13 20q16 20 16 25v54q0 6-17 30-10 13-11 17-5 10-8 23t-3 19.5-7 10.5l-6 2-6-2q-7-4-7-10.5t-3-19.5-8-23q-1-4-11-17-17-24-17-30v-54q0-5 16-25 13-16 13-20t-6.5-12.5-14-14T437 549l-10-6q-17-11-34-31-19-21-24-35l-10-39 6-9q8-11 16-16 17-11 30-11 2 0 14 12 21 23 33.5 28.5T482 451q8 2 14 2h8q6 0 14-2 11-3 23.5-8.5T575 414q12-12 14-12 13 0 30 11 8 5 16 16l6 9z',
+    bard: 'M368 653V341q0-5-5-9t-12-6l-6-1-6 1q-7 2-12 6t-5 9v312q0 8 12 13 5 2 11 3l6-1q7-2 12-6t5-9zm122 0V341q0-8-11-13-6-2-12-3l-6 1q-6 2-11.5 6t-5.5 9v312q0 5 5.5 9t11.5 6l6 1 6-1q7-2 12-6t5-9zm123 0V341q0-5-5-9t-12-6l-6-1-6 1q-7 2-12 6t-5 9v312q0 5 5 9t12 6l6 1 6-1q7-2 12-6t5-9zm119-538q-17 0-32.5 13.5T684 151t-8 11q-4 2-16 2H299q-18 0-34.5 7.5T235 192q-8 8-14 21.5t-6 23.5q0 20 5 30 8 13 27 13 14 0 25-7 7-4 17-14 7-7 11-9 6-4 11-4h328q16 0 31 10 18 13 18 35v346q0 14-15 38-16 26-41 49-28 27-61 43-39 18-79 18-115 0-195-60l-2-2q-9-6-18-8.5t-14 3 0 23.5 14 28q12 13 35 30 35 27 75 44 52 22 105.5 22T613 829q65-35 109-86 47-55 47-102V304q0-11 8-28.5t8-28.5v-82q0-15-18-32.5T732 115z',
+    whiteMage:
+      'M464 365q27 0 46-19t19-46-19-46.5-46-19.5-46.5 19.5T398 300t19.5 46 46.5 19zm58-239q-13-2-23 1t-12 10.5 1.5 15T499 165l8 4q34 15 54 36 37 38 37 97 0 35-16.5 67T537 420q-30 21-65 21-39 0-69-12-18-8-41-25l-5-4q-10-7-18-6-5 1-8 5-7 7-3 24 3 14 10 21 32 39 103 67l3 1q11 4 16 8 10 7 10 17v266q0 26 10 49t19.5 23 20-23 10.5-49V540q0-9 7.5-22.5T555 500q17-9 39-28 33-29 53-66 26-49 26-107 0-44-21-82-20-37-55-61t-75-30z',
+    blackMage:
+      'M783 146q-17 12-90 64-68 49-84 61-50 38-71 19-11-10-13-29-1-17 4.5-31t25.5-31l5-5q4-4 3.5-16t-6.5-19q-5-4-21-2t-36 11q-22 11-42 26l-11 9q-55 42-89 73-46 43-96 103-27 34-47 62-54 45-76.5 111t-7 134 65 117.5 118 65 134.5-7T559 785q31-22 64-49 58-48 100-94 31-33 72-87l10-12q15-20 26-43 8-20 10.5-36t-1.5-20q-7-6-19-7t-16 4l-5 5q-17 20-31 25.5t-31 3.5q-19-1-29-12-19-21 19-71 12-18 61-84 52-73 64-91 10-16 16-52 4-20 5-39l1-1-18 2q-21 2-38 6-24 6-36 13zM369 797q-50 0-92-28t-61.5-74.5-9.5-96 45.5-85.5 85.5-46 96.5 9.5T508 538t28 93q0 45-22.5 83T453 774.5 369 797zm305-419q-28 42-35 71-9 36 12 59 11 13 40 14 21 1 48-3l23-5q-10 16-24 30l-8 10q-26 34-47 55-27 27-73 65 11-59-7-116.5t-60.5-100-100-60.5-116.5-7q38-47 66-74 21-21 54-47l10-8q14-14 31-25l-5 24q-5 27-4 48 2 29 15 40 23 21 59 12 29-7 70-35 40-26 198-145l-1 1Q699 341 674 378z',
+    summoner:
+      'M776 347q-2-4-4-11-4-10-9-13-6-4-25.5-5.5T712 343q-3 16-2 51 1 14 0 19-1 15-18 21-10 4-34 5h-1q-9 1-34-12-15-8-53-30-45-27-67-38-27-13-81-54-31-24-91-73-46-37-56-43l-16-12q-19-14-26-16-11-3-19 9 44 103 76 200 26 81 42 157 12 60 18 113 3 39 3 70l-1 23-91 1q-23 0-27.5 7.5T237 768q3 5 30 13 31 9 69 15 49 6 95 4 57-2 105-17 57-17 99-51 83-83 121-167 29-65 30-129 1-36-8-82zM412 710q-4-102-20-194-19-110-50-175l25 23q31 28 56 48 36 28 57 36 9 4 36 13 61 21 89 34 48 22 55 43l-9 14q-26 42-46 65-34 40-75 63-53 28-118 30z',
+    scholar:
+      'M839 442q-6-9-15-18-36-11-49 17-6 13-7 34-1 18 2 24 13 30 13 48 0 33-24 57t-57.5 24-57.5-24-24-57q0-36 15-64 13-22 38-42 15-13 49-33 24-15 36-23l-4-3q-25-19-43-29-32-16-76-24-55-11-135-11t-135 11q-44 8-76 24-18 10-43 29l-4 3q12 8 36 23 34 20 49 33 25 20 38 42 15 28 15 64 0 34-24 57.5T298.5 628 241 604.5 217 547q0-18 13-48 3-6 2-24-1-21-7-34-13-28-49-17-9 9-15 18-15 22-25 49-11 30-11 56 0 61 38 109t98 61 114.5-14 80.5-82.5 11-115-63-96.5q23-9 49-13 17-3 47-4 30 1 47 4 26 4 49 13-49 37-63.5 96.5t11.5 115 80.5 82.5T739 716.5t98-61T875 547q0-26-11-56-10-27-25-49z',
+    ninja:
+      'M583 698q8-5 14-11 10-9 18-18 16-19 28-40 35-61 35-129 1-93-61-190-34-53-82-90-41-31-92-51-40-16-85-24-31-5-62-6l-25 1 17 2q20 4 43 12 31 11 60 28 37 22 70 51 39 34 70 78-8-3-17-6l-9-3q-12-3-26-5-18-2-38-2-24 0-47 4-28 5-54 15-29 13-56 32-30 22-55 53-28 34-51 78-46 88-39 187 4 71 35 141 15 35 29 56l-6-16q-7-20-11-43-6-32-6-66 0-43 9-86 10-51 33-100 1 9 3 18 4 20 12 38 4 9 8 17 32 64 91 102 82 54 206 48 62-2 119-26 47-19 90-53 33-27 62-62 21-25 37-51l12-21-11 12q-14 16-31 32-26 21-55 38-37 22-78 35-49 16-104 21zm-137-36q-39 0-71.5-19T323 591.5t-19-71 19-71.5 51.5-52 71-19 71 19 51.5 52 19 71.5-19 71-51.5 51.5-70.5 19z',
+    machinist:
+      'M756 479q-15-15-32-27-207-147-414-293l-1-1q-19-13-29-16-29-8-46 22-14 23 10 42 10 8 32 23l95 68q154 108 230 163l18 12q16 10 24 16 14 11 23 23-9 8-20 15-37 18-51 35t-10 38 27 54q14 20 31 38 14 14 11 28-3 12-18 24-13 10-18 12t-10-4q-4-4-16-23-23-34-33-53-16-33-45-17-22 13-7 43 24 48 37 71 22 39 45 66 18 22 30 22t30-23q13-17 37-52l11-16q22-32 26-42 6-15-1-29-5-10-30-40l-3-4q-10-12-26-30-20-21-22-28-3-9 10-16 10-6 51-21l5-1q11-5 34-16l3-2q27-14 27-30 1-13-15-31zm-205 59q6-26-.5-44T522 462q-40-27-119-83l-109-76q-11-8-22.5-8.5t-15 10.5 0 26 10.5 23 15 12q7 4 12 7 115 83 176 124l2 1q19 13 26 21 12 15 9 31-2 11-10 22t-17.5 13-18.5-4.5-8-16.5q1-26-10-39-8-11-32-20-8 21-10 46-1 14 1 43l1 10q2 23 14 31 11 6 34 2 34-5 52.5-14.5t29-28.5 18.5-56z',
+    darkKnight:
+      'M323 527q73-22 101-52 30-33 27-92-36 10-111 43-50 21-75 30-42 16-75 23 20-23 56-48 21-15 66-43 65-40 91-62-46-31-72-53-35-30-57-61 39-2 79 12 24 8 69 33l29 15q10-20 16-47 4-16 8.5-49.5T483 126q6-27 15-47 11 24 18 55 5 19 11 57 9 54 16 78 19-7 54-22 40-17 60-23 35-11 67-13-21 34-56 62-28 24-74 51 26 22 92 64 45 28 66 43 36 26 58 48-38-7-91-27-31-12-94-39t-88-35q-5 54 31.5 94T677 529q-13 18-44 56-37 43-52 66-26 38-33 71l-2 9q-14 63-22 95-14 52-29 95l-13-62q-19-90-30-135-9-38-41-86-19-28-64-82zm175-48q-12 6-23 15-6 6-16 18-9 10-14 15-15 15-4 32 10 14 32 49l19 29q32-36 45-57.5t12.5-36.5-14.5-31q-9-10-37-33z',
+    astrologian:
+      'M519 328q-115 0-229 1l-12-1q-12 0-17 1-7 2-10.5 10.5T247 367q-2 60 1 171 0 12 8.5 19.5t21 7 21-8T307 537q2-29 3-88v-1q0-28 4.5-40t13.5-15q5-1 21 0h3q48 3 96 2 17-1 25 3 11 4 16 17.5t5 41.5q0 21 1.5 63.5T497 584v50q1 42-1 53-3 15-17 18-9 3-46 2-33-2-55-2h-16q-31 1-39-1-11-3-14-13-1-7-1-33l1-33q0-2-5-6l-6-7q-125 78-160 212 54-55 94-88l7-6q11-12 15-13 8-1 3 22-4 21 20 28 14 4 46 4h9q42 1 127 0h32q33-1 47-5.5t19-18.5 6-45q1-94 3-280v-7q0-17-3-44-4-44-44-43zm166 22q-7-9-8-24-1-9 1-27 1-12 1-18-2-34-6-45-4-14-19-18-11-3-47-3l-144 1h-53q-16 1-24 9t-8 23q1 26 29 28 12 1 35 2l137 3q21 0 31 11t9 33q-1 51 0 154v126q0 12 3 18 5 8 16 9 28 4 36-20 4-15 4-31l1-19q1-39-2-58 4-48 13-72 11-28 39-47 19-12 59-34 48-26 74-44-50-11-99 2-46 12-78 41z',
+    samurai:
+      'M774 739q-64 73-155 102-87 29-180 11-37-7-65-20-52-23-24-51 7 4 43 9 28 4 62 6 64 4 125-18 64-24 113-72 29-32 35-73 7-46-22-84-17-23-45-33.5t-49-2.5q-15 6-24 20-5 9-7 19v-1q6 41 11 82 6 26-5 34-6 5-24 5H440q-18 0-24-5-11-8-5-34l4-34q3-33-2.5-48T393 531q-8-3-30-4t-34-3q-20-3-37-12-10-5-27-21.5T240 470q-12-5-18 6-8 13-11 52 5 79 48 133 14 11 17 21 2 5 0 14l-1 5q-1 5-4 10-6 7-16 5-14-3-35-24-50-55-69.5-133T148 403q17-81 68-139.5T338 173q69-31 144.5-32T628 170q23 10 38.5 24t11 21-26.5 6-43-9q-71-23-149-6-17 5-36 17-11 7-29 22-14 11-19 15-17 12-41 21-19 8-29 15l4-2q-8 5-15 11-4 4-10 12 6-7-6 6-16 25-10 54 6 26 28 47 36 27 69 32t56-9.5 30-44.5q25-97 50-111h1q28 16 54 130l3 10q2 11 5 15 4 7 13 7 12-3 27-6.5t23-4.5q11-1 32 1 23 2 51 16 22 11 42 26l1 1q10 11 16 11 11 0 14-32v1q-4-50-20-112-8-32-9-42 0-17 11-26 7-2 16.5 6.5T805 323q41 65 51 139.5t-11 147T774 739zM525 577q-11-137-24-150-13 13-24 150-1 10 2.5 16.5T491 600h21q7 0 10.5-6.5T525 577z',
+    redMage:
+      'M693 554q-5-2-11.5-6.5T669 542q-8-3-23-3-35 0-64 18.5T536.5 607 520 674q0 55 32 88.5t87 42.5q11 1 19.5 12t6.5 27-22 19q-15 1-38-5-68-13-107-60-42-50-42-129 0-49 22-91.5t62-70.5l2-1q25-14 28-51 7-130 21-275l3-27q3-33 5-43 3-16 10-16 6 0 10 15 2 11 5 44l2 27q11 105 19 227 3 33 12 59 8 24 15 30 10 4 29 15 17 10 24 13 12 5 19 4 6 1 4 7t-10.5 12.5-20.5 9-24-2.5zm-362-87q3-3 16 5 8 4 27 18l21 15q13 8 28 13l6 2q13 0 23 6.5t5 12.5q-33 40-42.5 102T435 765q16 32 43 64l1 2q0 8-11.5 5t-25-12-17.5-15q-11-9-23-26-7-6-17-1l-21 14q-19 14-27 18-13 8-16 5-3-4 5-17 4-8 19-28l14-20 4-7q3-6 3.5-15t-.5-15q-3-11-5-23v-1q-3-22-15-25l-8-2-27-5q-30-5-41-9-18-5-19-11 1-6 19-11 11-4 41-9l27-5 10-3q7-2 14.5-8.5T373 591q3-11 9-21 1-3 1-5-2-15-15-33l-14-21q-14-20-19-28-7-13-4-16zm240 204q0-19 11-35t29-23.5 37-4 32.5 17.5 17.5 33-3.5 37-23.5 29-36 11q-27 0-45.5-19T571 671z',
+    blueMage:
+      'M868 301q-2 0-13.5 2t-20.5 3q-2-33-29.5-63.5T745 206q-20-3-39 7-16 9-24 23.5t0 21.5q2 2 13 4 15 2 22 5 13 6 17 19 8 22 2 36.5T716 342l-97 29q-11 4-30 11-24 9-38 12-23 6-51 6t-51-6q-14-3-37-12-19-7-29-10l-32-10q-96-29-138-41-72-21-81-20-37 4-14 71l1 2q24 75 61 134 51 82 105 89 42 6 79-5 23-6 60-26 24-12 36-17 21-9 40-10 18 1 38 9 12 5 34 16-17 32-50 94-38 73-43 84l-7 15q-7 15-9 22-3 10 2.5 14t16.5-4q6-5 16-16l3-3q19-19 68-78 64-75 84-93 31 2 62-2 54-7 105-89 37-59 61-134l1-2q8-23 9-39 1-29-23-32zM290 521q-32-4-56-33-19-22-31-56-9-28-8-44 1-11 58 6 48 15 108 41.5t69 36-12.5 23T362 516q-39 9-72 5zm420 0q-33 4-72-5-34-8-55.5-21.5t-12.5-23 69-36T747 394q57-17 58-6 1 16-8 44-12 34-31 56-24 29-56 33z',
+    gunbreaker:
+      'M820 222q-34 48-68 95l-2 5q-4 7-13 18-12 14-32 34L366 710q46 29 99.5 33t103-17.5 83.5-63 45-94.5q4-23 18-33 11-8 24.5-7t23.5 11q13 12 10 31-14 90-67 150 0 5 4 19 7 24 7 36 1 20-10 29-10 8-25 2-9-3-28-14-12-7-16-9-8-4-11-2-77 43-164.5 37.5T305 760l-1 1q-68 49-93 65-13 9-21 11-7 1-10 0l-6-6q-2-4 0-10 2-9 10-21 20-28 68-95l3-5q4-7 12-18 13-15 33-35l306-303q-50-34-111-38t-115 24.5-85.5 81T263 525q0 17-7 30-8 14-25.5 15.5t-30.5-12-13-33.5q0-66 27-124t76-100 111-60q3-1 6-8 2-5 6-18 7-24 12-34 8-18 20-19t26 16q7 10 20 33 11 20 14 20 87 6 156 59 29-26 37-27l96-68q13-8 21-10 6-2 10 0l6 6q2 4 0 10-2 9-11 21z',
+    dancer:
+      'M801 424q-2 36-13 62.5T754 542q-15 19-58 65l-19 19q-54 59-91 108-21 29-32 49-7 13-17.5 39T520 858q-10 14-22 14t-22-14q-5-10-16-36t-18-39q-34-61-123-157l-19-19q-43-46-58-65-23-29-34-55.5T195 424q-4-53 22-90 23-34 60-43 50-12 96-43 74-50 125-143 51 93 125 143 46 31 96 43 37 9 60 43 26 37 22 90zm-65-15q-3-31-20-49-15-15-37.5-18t-44.5 5q-30 9-37 43-4 20 0 61 2 22 2 29-1 12-7 13-15 3-32-16-16-16-28-41.5T516 392q-1-6 1-17 2-15 8-28 6-16 28-33 19-14 28-15-10-3-47-41l-36-38h-1q-67 74-82 79 9 1 28 15 22 17 28 33 6 13 8 28 2 11 1 17-3 18-15.5 43.5T437 477q-18 19-33 16-6-1-7-13 0-7 2-29 4-41 0-61-7-34-37-43-22-8-44.5-5T280 360q-17 18-20 49-4 38 21 79 15 24 55 68l16 17q33 37 87 85 28 26 38 37 16 17 21 32 5-15 21-32 10-11 38-37 54-48 87-85l16-17q40-43 55-68 25-41 21-79z',
+    reaper:
+      'M846 560q-2-4-11 0-6 2-21 12-76 48-180 79-107 31-182 25-58-4-98-19l47-150q22 54 72 83 40 23 85 22.5t84-22.5 62-62q22-37 23-79t-17-79.5-52-62.5q-1-1-1-17 0-18-2-26-2-15-9-18t-20 5q-7 5-20 18-10 9-11 9-34-8-67.5-1.5T465 301l36-116q6-16 9-39 4-33-6-36.5T477 135q-11 20-16 36L299 622q-17-17-24-39t-5-51q1-18 7-51l1-5q5-26-.5-28.5t-20 20T232 516q-15 33-19 61-5 41 7 77 13 41 48 67l-18 78q-5 15-7 33-2 27 8.5 30.5T276 843q10-15 15-30l30-66q17 5 32 8 63 10 139-2 83-12 164-50 89-41 164-108 16-14 21-21 8-10 5-14zM466 390q15-24 39-38t52.5-14.5 53 14 38.5 39 14 52.5-14 53-38.5 38.5-52.5 14-53-14-39-39-14-52.5 14-53z',
+    sage: 'M872 223q-18 33-56 96-49 82-63 112-4 10-4 28 1 11 0 18-2 31-10.5 47.5T721 531l-10-11q-26-31-40-42-24-17-56-23-21-3-20-13.5t17.5-23T651 397q4-2 10-4 13-3 25-10 15-12 36-36 12-15 39-49 52-66 80-93 16-11 25.5-6t5.5 24zM563 373q-18 0-31.5-9.5t-13.5-23 13.5-23 32-9.5 32 9.5 13.5 23-13.5 23T563 373zm-135 87q-26 0-45-13.5T364 414t19-32.5 45.5-13.5 45.5 13.5 19 32.5-19 32.5-46 13.5zm41-192q-64 31-116 79-58 53-84 110-29 65-13 126 6 20 17 38 7 11 21 27 16 19 21 30 7 17 2 41-14 36-42 35-70 0-105-111-25-79 5-159 28-78 100-140t171-91q12-2 21.5-.5t10 6-8.5 9.5zM364 584q-25 0-42-12.5T305 542t17-29.5 41.5-12.5 42 12.5T423 542t-17.5 29.5T364 584zm58 111q6-17 15-47 13-43 21-62 14-31 36-54 25-28 69-53 27-6 58 6 28 11 50.5 34t31.5 48q-7 85-64 142-53 52-132.5 68T348 768q1-12 11-22 6-5 22.5-14t24.5-14q12-10 16-23zm60 22q18 5 49-1 28-8 55-27 30-22 45-49 17-32 7-63-5-12-17.5-23T592 539q-19-4-29 5-83 71-81 173z',
+    viper:
+      'M680 161q2 42-2 63.5T659 268q-13 18-22.5 25t-22.5 9q-8 1-30 1h-12q-17 53-22 87-4 31 2 56 3 16 15 48 18 44 26 79 27 69-3 274 27-22 45-57 16-30 27-74 7-26 18-80 9-46 14-67 9-36 19-61 8-14 33 10 29-111 12-195-16-80-78-162zm15 140q13 49 15 74 2 35-8 69-25 0-38 17-11 15-15 44-3 17-3 57l-1 23-3 8q-4 12-6 15-3 5-3-5 2-44-4-78-4-20-14.5-49T602 430q-2-28 8-67 11 0 29-8t32-21q18-15 24-33zM316 161q-2 42 2.5 63.5T337 268q13 18 23 25t22 9q9 1 31 1h12q17 53 21 87 4 31-1 56-3 16-16 48-17 44-26 79-27 69 4 274-27-22-46-57-15-30-27-74-7-26-18-80-8-46-14-67-8-36-19-61-8-14-33 10-28-111-12-195 16-80 78-162zm-15 140q-12 49-14 74-3 35 7 69 26 0 39 17 11 15 14 44 3 17 4 57v23l3 8q5 12 6 15 3 5 3-5-2-44 5-78 3-20 13.5-49t12.5-46q3-28-7-67-12 0-29.5-8T325 334q-17-15-24-33z',
+    pictomancer:
+      'M872 223q-18 33-56 96-49 82-63 112-4 10-4 28 1 11 0 18-2 31-10.5 47.5T721 531l-10-11q-26-31-40-42-24-17-56-23-21-3-20-13.5t17.5-23T651 397q4-2 10-4 13-3 25-10 15-12 36-36 12-15 39-49 52-66 80-93 16-11 25.5-6t5.5 24zM563 373q-18 0-31.5-9.5t-13.5-23 13.5-23 32-9.5 32 9.5 13.5 23-13.5 23T563 373zm-135 87q-26 0-45-13.5T364 414t19-32.5 45.5-13.5 45.5 13.5 19 32.5-19 32.5-46 13.5zm41-192q-64 31-116 79-58 53-84 110-29 65-13 126 6 20 17 38 7 11 21 27 16 19 21 30 7 17 2 41-14 36-42 35-70 0-105-111-25-79 5-159 28-78 100-140t171-91q12-2 21.5-.5t10 6-8.5 9.5zM364 584q-25 0-42-12.5T305 542t17-29.5 41.5-12.5 42 12.5T423 542t-17.5 29.5T364 584zm58 111q6-17 15-47 13-43 21-62 14-31 36-54 25-28 69-53 27-6 58 6 28 11 50.5 34t31.5 48q-7 85-64 142-53 52-132.5 68T348 768q1-12 11-22 6-5 22.5-14t24.5-14q12-10 16-23zm60 22q18 5 49-1 28-8 55-27 30-22 45-49 17-32 7-63-5-12-17.5-23T592 539q-19-4-29 5-83 71-81 173z',
+  },
+};
+
+const ffxivJobIconPaths = Object.values(ffxivJobIconData.paths);
path: packages/ui-craft/react/ffxiv-job-icons-loop-using-flubber.tsx
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**Object.values()를 사용할 때 순서 의존성을 주의하세요**  
객체의 키 순서는 보장되지 않으므로, 특정 순서대로 애니메이션을 진행해야 한다면 배열 기반이 더 안전합니다.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-25T12:18:23Z
author_association: NONE
diff_hunk: @@ -0,0 +1,122 @@
+'use client';
+
+import { interpolate } from 'flubber';
+import { animate } from 'framer-motion';
+import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
+
+export function FfxivJobIconsLoopUsingFlubber() {
+  const path = useSvgPathMorphing(ffxivJobIconPaths);
+  return (
+    <svg viewBox={ffxivJobIconData.viewBox} width={80} height={80}>
+      <title>FFXIV Job Icons Loop</title>
+      <g fill="currentColor">
+        <path d={path} />
+      </g>
+    </svg>
+  );
+}
+
+function useSvgPathMorphing(pathList: string[]) {
+  if (pathList.length < 2) {
+    throw new Error('useSvgPathMorphing: pathList length must be at least 2');
+  }
+
+  const [[from, to], increment] = useIncrementingTuple(pathList.length);
+
+  const interpolater = useMemo(() => {
+    const fromPath = pathList[from];
+    const toPath = pathList[to];
+    if (!fromPath || !toPath) {
+      throw new Error('Invalid path index');
+    }
+    return interpolate(fromPath, toPath);
+  }, [from, to, pathList]);
+
+  const [currentPath, setCurrentPath] = useState(pathList[from]);
+
+  useLayoutEffect(() => {
+    const animation = animate<number>(0, 1, {
+      duration: 0.2,
+      delay: 0.3,
+      ease: 'easeIn',
+      onUpdate: (progress) => {
+        setCurrentPath(interpolater(progress));
+      },
+      onComplete: () => {
+        increment();
+      },
+    });
+    return () => animation.stop();
+  }, [increment, interpolater]);
+
+  return currentPath;
+}
+
+export function useIncrementingTuple(
+  size: number,
+): [[number, number], () => void] {
+  const [tuple, setTuple] = useState<[number, number]>(
+    size > 1 ? [0, 1] : [0, 0],
+  );
+
+  const increment = useCallback(() => {
+    if (size <= 1) return;
+
+    setTuple(([a, b]) => {
+      const nextA = (a + 1) % size;
+      const nextB = (b + 1) % size;
+      return [nextA, nextB];
+    });
+  }, [size]);
+
+  return [tuple, increment];
+}
+
+const ffxivJobIconData = {
+  viewBox: '0 0 1024 1024',
+  paths: {
+    paladin:
+      'M757 139q-5 0-14 10-6 6-18.5 21.5T707 192q-9 10-14 10H555q-4 0-20-34l-6-13q-6-12-14-20-5-6-11-8l-4-2-4 2q-6 2-11 8-8 8-14 20l-6 13q-16 34-20 34H307q-5 0-14-10-5-6-17.5-21.5T257 149q-9-10-14-10-14 0-26 13-14 14-14 35v258q0 96 33 177 31 76 88 130 48 45 115 88 34 21 61 35 26-13 61-35 67-43 115-88 57-54 88-130 33-81 33-177V187q0-21-14-35-12-13-26-13zM469 768q-11-6-21-12-26-17-49-37-34-29-59-61-31-40-47-94-12-42-12-80l1-219h187v503zm191-110q-25 32-59 61-23 20-49 37l-21 12V265h187l1 219q0 38-12 80-16 54-47 94z',
+    monk: 'M525 235q21-18 36-39 12-17 19-35 6-13 9-26l1-10h-10q-12 1-26 4-20 4-38 13-24 11-45 29L190 407q-20 17-35 38-12 17-20 35-6 13-8 26l-2 10h10q13-1 27-4 19-4 38-13 23-11 44-29l140-118zm142 169q21-17 36-38 12-17 20-36 5-13 8-25l2-10h-10q-13 1-27 4-19 4-38 13-23 11-44 29L333 576q-21 18-36 39-12 17-20 35-5 13-8 26l-2 9h10q13 0 27-3 19-5 38-13 23-11 44-29l141-118zm89 106L616 628 475 746q-21 18-36 39-12 16-19 35-6 13-9 25l-1 10h10q12 0 26-3 20-5 38-14 24-11 45-28l281-236q38-32 55-74 9-21 10-35h-10q-13 0-27 3-19 5-38 13-23 12-44 29z',
+    warrior:
+      'M875 500l-2-26q-3-31-12-63-11-44-31-82-17-31-45.5-62t-57-49-43-14-22.5 12q-11 11-16 32l-4 22q-4 21-9 32-7 17-25 38-22 25-67 33-23 3-41 2-20 1-43-2-44-8-65-33-18-21-25-38-5-11-9-32l-4-22q-5-21-16-31-8-9-22.5-13t-43 14-57 49-45.5 62q-20 38-31 82-9 32-12 64l-2 25q1 24 7 57 11 63 39 114 16 31 45 62t57 49 42.5 14 23.5-12q10-11 15-32l5-22q4-21 8-32 8-17 25-38 16-18 44-27 20-7 44-8h20q21-2 43 2 44 8 65 33 18 21 25 38 5 11 9 32l4 22q5 21 16 31 8 9 22.5 13t43-14.5 57-49T830 670q20-37 31-81 9-32 12-64zM760 627q-9 18-22 35-9 12-18 22l-8 7-3-11q-5-15-12-29-10-19-23-35-16-19-36-32-24-16-51-25-22-7-44-11-16-2-31-2l-12 1q-18-2-45 2-50 8-92 35-37 23-59 68-12 22-16 40l-7-8q-10-10-19-22-12-17-22-35-13-26-20-58-5-24-6-49l-1-20q-1-19 2-44 6-48 25-83 9-18 22-35 9-12 18-22l8-7 3 11q5 15 12 28 10 20 23 36 16 19 36 32 44 28 95 35 26 4 43 2 19 2 44-2 51-8 94-35 20-13 36-33 13-15 23-35 7-14 12-28l3-12 8 8q9 10 18 22 13 17 22 35 19 35 25 82 3 25 2 45v1q1 22-3 46-6 46-24 80z',
+    dragoon:
+      'M723 335h-38l-8 10q-9 12-18 19-11 10-17 10-11 0-18-14t-7-28V171q0-13-7-30-4-13-12-25l-7-11-7 12q-7 13-12 26-7 17-7 28v172l-5 7q-7 8-19 16-19 13-41 16-22-3-41-16t-24-23V171q0-11-7-28-5-13-12-26l-7-12-7 11q-8 12-12 25-7 17-7 30v161q0 14-7 28t-18 14q-6 0-17-10-9-7-18-19l-8-10h-38q-6 0-10.5 4t-3.5 10q0 4 72 165 53 117 58 130 7 19 41 122l19 56q5 13 14 23t16 10l17 1 17-1q7 0 16-10t14-23l19-56q34-103 41-122 5-13 58-130 72-161 72-165 0-6-4-10t-10-4zm-92 142q-5 14-24 35-17 20-34 31l-10 6q-12 7-19.5 12.5t-14 14T523 588t13 20q16 20 16 25v54q0 6-17 30-10 13-11 17-5 10-8 23t-3 19.5-7 10.5l-6 2-6-2q-7-4-7-10.5t-3-19.5-8-23q-1-4-11-17-17-24-17-30v-54q0-5 16-25 13-16 13-20t-6.5-12.5-14-14T437 549l-10-6q-17-11-34-31-19-21-24-35l-10-39 6-9q8-11 16-16 17-11 30-11 2 0 14 12 21 23 33.5 28.5T482 451q8 2 14 2h8q6 0 14-2 11-3 23.5-8.5T575 414q12-12 14-12 13 0 30 11 8 5 16 16l6 9z',
+    bard: 'M368 653V341q0-5-5-9t-12-6l-6-1-6 1q-7 2-12 6t-5 9v312q0 8 12 13 5 2 11 3l6-1q7-2 12-6t5-9zm122 0V341q0-8-11-13-6-2-12-3l-6 1q-6 2-11.5 6t-5.5 9v312q0 5 5.5 9t11.5 6l6 1 6-1q7-2 12-6t5-9zm123 0V341q0-5-5-9t-12-6l-6-1-6 1q-7 2-12 6t-5 9v312q0 5 5 9t12 6l6 1 6-1q7-2 12-6t5-9zm119-538q-17 0-32.5 13.5T684 151t-8 11q-4 2-16 2H299q-18 0-34.5 7.5T235 192q-8 8-14 21.5t-6 23.5q0 20 5 30 8 13 27 13 14 0 25-7 7-4 17-14 7-7 11-9 6-4 11-4h328q16 0 31 10 18 13 18 35v346q0 14-15 38-16 26-41 49-28 27-61 43-39 18-79 18-115 0-195-60l-2-2q-9-6-18-8.5t-14 3 0 23.5 14 28q12 13 35 30 35 27 75 44 52 22 105.5 22T613 829q65-35 109-86 47-55 47-102V304q0-11 8-28.5t8-28.5v-82q0-15-18-32.5T732 115z',
+    whiteMage:
+      'M464 365q27 0 46-19t19-46-19-46.5-46-19.5-46.5 19.5T398 300t19.5 46 46.5 19zm58-239q-13-2-23 1t-12 10.5 1.5 15T499 165l8 4q34 15 54 36 37 38 37 97 0 35-16.5 67T537 420q-30 21-65 21-39 0-69-12-18-8-41-25l-5-4q-10-7-18-6-5 1-8 5-7 7-3 24 3 14 10 21 32 39 103 67l3 1q11 4 16 8 10 7 10 17v266q0 26 10 49t19.5 23 20-23 10.5-49V540q0-9 7.5-22.5T555 500q17-9 39-28 33-29 53-66 26-49 26-107 0-44-21-82-20-37-55-61t-75-30z',
+    blackMage:
+      'M783 146q-17 12-90 64-68 49-84 61-50 38-71 19-11-10-13-29-1-17 4.5-31t25.5-31l5-5q4-4 3.5-16t-6.5-19q-5-4-21-2t-36 11q-22 11-42 26l-11 9q-55 42-89 73-46 43-96 103-27 34-47 62-54 45-76.5 111t-7 134 65 117.5 118 65 134.5-7T559 785q31-22 64-49 58-48 100-94 31-33 72-87l10-12q15-20 26-43 8-20 10.5-36t-1.5-20q-7-6-19-7t-16 4l-5 5q-17 20-31 25.5t-31 3.5q-19-1-29-12-19-21 19-71 12-18 61-84 52-73 64-91 10-16 16-52 4-20 5-39l1-1-18 2q-21 2-38 6-24 6-36 13zM369 797q-50 0-92-28t-61.5-74.5-9.5-96 45.5-85.5 85.5-46 96.5 9.5T508 538t28 93q0 45-22.5 83T453 774.5 369 797zm305-419q-28 42-35 71-9 36 12 59 11 13 40 14 21 1 48-3l23-5q-10 16-24 30l-8 10q-26 34-47 55-27 27-73 65 11-59-7-116.5t-60.5-100-100-60.5-116.5-7q38-47 66-74 21-21 54-47l10-8q14-14 31-25l-5 24q-5 27-4 48 2 29 15 40 23 21 59 12 29-7 70-35 40-26 198-145l-1 1Q699 341 674 378z',
+    summoner:
+      'M776 347q-2-4-4-11-4-10-9-13-6-4-25.5-5.5T712 343q-3 16-2 51 1 14 0 19-1 15-18 21-10 4-34 5h-1q-9 1-34-12-15-8-53-30-45-27-67-38-27-13-81-54-31-24-91-73-46-37-56-43l-16-12q-19-14-26-16-11-3-19 9 44 103 76 200 26 81 42 157 12 60 18 113 3 39 3 70l-1 23-91 1q-23 0-27.5 7.5T237 768q3 5 30 13 31 9 69 15 49 6 95 4 57-2 105-17 57-17 99-51 83-83 121-167 29-65 30-129 1-36-8-82zM412 710q-4-102-20-194-19-110-50-175l25 23q31 28 56 48 36 28 57 36 9 4 36 13 61 21 89 34 48 22 55 43l-9 14q-26 42-46 65-34 40-75 63-53 28-118 30z',
+    scholar:
+      'M839 442q-6-9-15-18-36-11-49 17-6 13-7 34-1 18 2 24 13 30 13 48 0 33-24 57t-57.5 24-57.5-24-24-57q0-36 15-64 13-22 38-42 15-13 49-33 24-15 36-23l-4-3q-25-19-43-29-32-16-76-24-55-11-135-11t-135 11q-44 8-76 24-18 10-43 29l-4 3q12 8 36 23 34 20 49 33 25 20 38 42 15 28 15 64 0 34-24 57.5T298.5 628 241 604.5 217 547q0-18 13-48 3-6 2-24-1-21-7-34-13-28-49-17-9 9-15 18-15 22-25 49-11 30-11 56 0 61 38 109t98 61 114.5-14 80.5-82.5 11-115-63-96.5q23-9 49-13 17-3 47-4 30 1 47 4 26 4 49 13-49 37-63.5 96.5t11.5 115 80.5 82.5T739 716.5t98-61T875 547q0-26-11-56-10-27-25-49z',
+    ninja:
+      'M583 698q8-5 14-11 10-9 18-18 16-19 28-40 35-61 35-129 1-93-61-190-34-53-82-90-41-31-92-51-40-16-85-24-31-5-62-6l-25 1 17 2q20 4 43 12 31 11 60 28 37 22 70 51 39 34 70 78-8-3-17-6l-9-3q-12-3-26-5-18-2-38-2-24 0-47 4-28 5-54 15-29 13-56 32-30 22-55 53-28 34-51 78-46 88-39 187 4 71 35 141 15 35 29 56l-6-16q-7-20-11-43-6-32-6-66 0-43 9-86 10-51 33-100 1 9 3 18 4 20 12 38 4 9 8 17 32 64 91 102 82 54 206 48 62-2 119-26 47-19 90-53 33-27 62-62 21-25 37-51l12-21-11 12q-14 16-31 32-26 21-55 38-37 22-78 35-49 16-104 21zm-137-36q-39 0-71.5-19T323 591.5t-19-71 19-71.5 51.5-52 71-19 71 19 51.5 52 19 71.5-19 71-51.5 51.5-70.5 19z',
+    machinist:
+      'M756 479q-15-15-32-27-207-147-414-293l-1-1q-19-13-29-16-29-8-46 22-14 23 10 42 10 8 32 23l95 68q154 108 230 163l18 12q16 10 24 16 14 11 23 23-9 8-20 15-37 18-51 35t-10 38 27 54q14 20 31 38 14 14 11 28-3 12-18 24-13 10-18 12t-10-4q-4-4-16-23-23-34-33-53-16-33-45-17-22 13-7 43 24 48 37 71 22 39 45 66 18 22 30 22t30-23q13-17 37-52l11-16q22-32 26-42 6-15-1-29-5-10-30-40l-3-4q-10-12-26-30-20-21-22-28-3-9 10-16 10-6 51-21l5-1q11-5 34-16l3-2q27-14 27-30 1-13-15-31zm-205 59q6-26-.5-44T522 462q-40-27-119-83l-109-76q-11-8-22.5-8.5t-15 10.5 0 26 10.5 23 15 12q7 4 12 7 115 83 176 124l2 1q19 13 26 21 12 15 9 31-2 11-10 22t-17.5 13-18.5-4.5-8-16.5q1-26-10-39-8-11-32-20-8 21-10 46-1 14 1 43l1 10q2 23 14 31 11 6 34 2 34-5 52.5-14.5t29-28.5 18.5-56z',
+    darkKnight:
+      'M323 527q73-22 101-52 30-33 27-92-36 10-111 43-50 21-75 30-42 16-75 23 20-23 56-48 21-15 66-43 65-40 91-62-46-31-72-53-35-30-57-61 39-2 79 12 24 8 69 33l29 15q10-20 16-47 4-16 8.5-49.5T483 126q6-27 15-47 11 24 18 55 5 19 11 57 9 54 16 78 19-7 54-22 40-17 60-23 35-11 67-13-21 34-56 62-28 24-74 51 26 22 92 64 45 28 66 43 36 26 58 48-38-7-91-27-31-12-94-39t-88-35q-5 54 31.5 94T677 529q-13 18-44 56-37 43-52 66-26 38-33 71l-2 9q-14 63-22 95-14 52-29 95l-13-62q-19-90-30-135-9-38-41-86-19-28-64-82zm175-48q-12 6-23 15-6 6-16 18-9 10-14 15-15 15-4 32 10 14 32 49l19 29q32-36 45-57.5t12.5-36.5-14.5-31q-9-10-37-33z',
+    astrologian:
+      'M519 328q-115 0-229 1l-12-1q-12 0-17 1-7 2-10.5 10.5T247 367q-2 60 1 171 0 12 8.5 19.5t21 7 21-8T307 537q2-29 3-88v-1q0-28 4.5-40t13.5-15q5-1 21 0h3q48 3 96 2 17-1 25 3 11 4 16 17.5t5 41.5q0 21 1.5 63.5T497 584v50q1 42-1 53-3 15-17 18-9 3-46 2-33-2-55-2h-16q-31 1-39-1-11-3-14-13-1-7-1-33l1-33q0-2-5-6l-6-7q-125 78-160 212 54-55 94-88l7-6q11-12 15-13 8-1 3 22-4 21 20 28 14 4 46 4h9q42 1 127 0h32q33-1 47-5.5t19-18.5 6-45q1-94 3-280v-7q0-17-3-44-4-44-44-43zm166 22q-7-9-8-24-1-9 1-27 1-12 1-18-2-34-6-45-4-14-19-18-11-3-47-3l-144 1h-53q-16 1-24 9t-8 23q1 26 29 28 12 1 35 2l137 3q21 0 31 11t9 33q-1 51 0 154v126q0 12 3 18 5 8 16 9 28 4 36-20 4-15 4-31l1-19q1-39-2-58 4-48 13-72 11-28 39-47 19-12 59-34 48-26 74-44-50-11-99 2-46 12-78 41z',
+    samurai:
+      'M774 739q-64 73-155 102-87 29-180 11-37-7-65-20-52-23-24-51 7 4 43 9 28 4 62 6 64 4 125-18 64-24 113-72 29-32 35-73 7-46-22-84-17-23-45-33.5t-49-2.5q-15 6-24 20-5 9-7 19v-1q6 41 11 82 6 26-5 34-6 5-24 5H440q-18 0-24-5-11-8-5-34l4-34q3-33-2.5-48T393 531q-8-3-30-4t-34-3q-20-3-37-12-10-5-27-21.5T240 470q-12-5-18 6-8 13-11 52 5 79 48 133 14 11 17 21 2 5 0 14l-1 5q-1 5-4 10-6 7-16 5-14-3-35-24-50-55-69.5-133T148 403q17-81 68-139.5T338 173q69-31 144.5-32T628 170q23 10 38.5 24t11 21-26.5 6-43-9q-71-23-149-6-17 5-36 17-11 7-29 22-14 11-19 15-17 12-41 21-19 8-29 15l4-2q-8 5-15 11-4 4-10 12 6-7-6 6-16 25-10 54 6 26 28 47 36 27 69 32t56-9.5 30-44.5q25-97 50-111h1q28 16 54 130l3 10q2 11 5 15 4 7 13 7 12-3 27-6.5t23-4.5q11-1 32 1 23 2 51 16 22 11 42 26l1 1q10 11 16 11 11 0 14-32v1q-4-50-20-112-8-32-9-42 0-17 11-26 7-2 16.5 6.5T805 323q41 65 51 139.5t-11 147T774 739zM525 577q-11-137-24-150-13 13-24 150-1 10 2.5 16.5T491 600h21q7 0 10.5-6.5T525 577z',
+    redMage:
+      'M693 554q-5-2-11.5-6.5T669 542q-8-3-23-3-35 0-64 18.5T536.5 607 520 674q0 55 32 88.5t87 42.5q11 1 19.5 12t6.5 27-22 19q-15 1-38-5-68-13-107-60-42-50-42-129 0-49 22-91.5t62-70.5l2-1q25-14 28-51 7-130 21-275l3-27q3-33 5-43 3-16 10-16 6 0 10 15 2 11 5 44l2 27q11 105 19 227 3 33 12 59 8 24 15 30 10 4 29 15 17 10 24 13 12 5 19 4 6 1 4 7t-10.5 12.5-20.5 9-24-2.5zm-362-87q3-3 16 5 8 4 27 18l21 15q13 8 28 13l6 2q13 0 23 6.5t5 12.5q-33 40-42.5 102T435 765q16 32 43 64l1 2q0 8-11.5 5t-25-12-17.5-15q-11-9-23-26-7-6-17-1l-21 14q-19 14-27 18-13 8-16 5-3-4 5-17 4-8 19-28l14-20 4-7q3-6 3.5-15t-.5-15q-3-11-5-23v-1q-3-22-15-25l-8-2-27-5q-30-5-41-9-18-5-19-11 1-6 19-11 11-4 41-9l27-5 10-3q7-2 14.5-8.5T373 591q3-11 9-21 1-3 1-5-2-15-15-33l-14-21q-14-20-19-28-7-13-4-16zm240 204q0-19 11-35t29-23.5 37-4 32.5 17.5 17.5 33-3.5 37-23.5 29-36 11q-27 0-45.5-19T571 671z',
+    blueMage:
+      'M868 301q-2 0-13.5 2t-20.5 3q-2-33-29.5-63.5T745 206q-20-3-39 7-16 9-24 23.5t0 21.5q2 2 13 4 15 2 22 5 13 6 17 19 8 22 2 36.5T716 342l-97 29q-11 4-30 11-24 9-38 12-23 6-51 6t-51-6q-14-3-37-12-19-7-29-10l-32-10q-96-29-138-41-72-21-81-20-37 4-14 71l1 2q24 75 61 134 51 82 105 89 42 6 79-5 23-6 60-26 24-12 36-17 21-9 40-10 18 1 38 9 12 5 34 16-17 32-50 94-38 73-43 84l-7 15q-7 15-9 22-3 10 2.5 14t16.5-4q6-5 16-16l3-3q19-19 68-78 64-75 84-93 31 2 62-2 54-7 105-89 37-59 61-134l1-2q8-23 9-39 1-29-23-32zM290 521q-32-4-56-33-19-22-31-56-9-28-8-44 1-11 58 6 48 15 108 41.5t69 36-12.5 23T362 516q-39 9-72 5zm420 0q-33 4-72-5-34-8-55.5-21.5t-12.5-23 69-36T747 394q57-17 58-6 1 16-8 44-12 34-31 56-24 29-56 33z',
+    gunbreaker:
+      'M820 222q-34 48-68 95l-2 5q-4 7-13 18-12 14-32 34L366 710q46 29 99.5 33t103-17.5 83.5-63 45-94.5q4-23 18-33 11-8 24.5-7t23.5 11q13 12 10 31-14 90-67 150 0 5 4 19 7 24 7 36 1 20-10 29-10 8-25 2-9-3-28-14-12-7-16-9-8-4-11-2-77 43-164.5 37.5T305 760l-1 1q-68 49-93 65-13 9-21 11-7 1-10 0l-6-6q-2-4 0-10 2-9 10-21 20-28 68-95l3-5q4-7 12-18 13-15 33-35l306-303q-50-34-111-38t-115 24.5-85.5 81T263 525q0 17-7 30-8 14-25.5 15.5t-30.5-12-13-33.5q0-66 27-124t76-100 111-60q3-1 6-8 2-5 6-18 7-24 12-34 8-18 20-19t26 16q7 10 20 33 11 20 14 20 87 6 156 59 29-26 37-27l96-68q13-8 21-10 6-2 10 0l6 6q2 4 0 10-2 9-11 21z',
+    dancer:
+      'M801 424q-2 36-13 62.5T754 542q-15 19-58 65l-19 19q-54 59-91 108-21 29-32 49-7 13-17.5 39T520 858q-10 14-22 14t-22-14q-5-10-16-36t-18-39q-34-61-123-157l-19-19q-43-46-58-65-23-29-34-55.5T195 424q-4-53 22-90 23-34 60-43 50-12 96-43 74-50 125-143 51 93 125 143 46 31 96 43 37 9 60 43 26 37 22 90zm-65-15q-3-31-20-49-15-15-37.5-18t-44.5 5q-30 9-37 43-4 20 0 61 2 22 2 29-1 12-7 13-15 3-32-16-16-16-28-41.5T516 392q-1-6 1-17 2-15 8-28 6-16 28-33 19-14 28-15-10-3-47-41l-36-38h-1q-67 74-82 79 9 1 28 15 22 17 28 33 6 13 8 28 2 11 1 17-3 18-15.5 43.5T437 477q-18 19-33 16-6-1-7-13 0-7 2-29 4-41 0-61-7-34-37-43-22-8-44.5-5T280 360q-17 18-20 49-4 38 21 79 15 24 55 68l16 17q33 37 87 85 28 26 38 37 16 17 21 32 5-15 21-32 10-11 38-37 54-48 87-85l16-17q40-43 55-68 25-41 21-79z',
+    reaper:
+      'M846 560q-2-4-11 0-6 2-21 12-76 48-180 79-107 31-182 25-58-4-98-19l47-150q22 54 72 83 40 23 85 22.5t84-22.5 62-62q22-37 23-79t-17-79.5-52-62.5q-1-1-1-17 0-18-2-26-2-15-9-18t-20 5q-7 5-20 18-10 9-11 9-34-8-67.5-1.5T465 301l36-116q6-16 9-39 4-33-6-36.5T477 135q-11 20-16 36L299 622q-17-17-24-39t-5-51q1-18 7-51l1-5q5-26-.5-28.5t-20 20T232 516q-15 33-19 61-5 41 7 77 13 41 48 67l-18 78q-5 15-7 33-2 27 8.5 30.5T276 843q10-15 15-30l30-66q17 5 32 8 63 10 139-2 83-12 164-50 89-41 164-108 16-14 21-21 8-10 5-14zM466 390q15-24 39-38t52.5-14.5 53 14 38.5 39 14 52.5-14 53-38.5 38.5-52.5 14-53-14-39-39-14-52.5 14-53z',
+    sage: 'M872 223q-18 33-56 96-49 82-63 112-4 10-4 28 1 11 0 18-2 31-10.5 47.5T721 531l-10-11q-26-31-40-42-24-17-56-23-21-3-20-13.5t17.5-23T651 397q4-2 10-4 13-3 25-10 15-12 36-36 12-15 39-49 52-66 80-93 16-11 25.5-6t5.5 24zM563 373q-18 0-31.5-9.5t-13.5-23 13.5-23 32-9.5 32 9.5 13.5 23-13.5 23T563 373zm-135 87q-26 0-45-13.5T364 414t19-32.5 45.5-13.5 45.5 13.5 19 32.5-19 32.5-46 13.5zm41-192q-64 31-116 79-58 53-84 110-29 65-13 126 6 20 17 38 7 11 21 27 16 19 21 30 7 17 2 41-14 36-42 35-70 0-105-111-25-79 5-159 28-78 100-140t171-91q12-2 21.5-.5t10 6-8.5 9.5zM364 584q-25 0-42-12.5T305 542t17-29.5 41.5-12.5 42 12.5T423 542t-17.5 29.5T364 584zm58 111q6-17 15-47 13-43 21-62 14-31 36-54 25-28 69-53 27-6 58 6 28 11 50.5 34t31.5 48q-7 85-64 142-53 52-132.5 68T348 768q1-12 11-22 6-5 22.5-14t24.5-14q12-10 16-23zm60 22q18 5 49-1 28-8 55-27 30-22 45-49 17-32 7-63-5-12-17.5-23T592 539q-19-4-29 5-83 71-81 173z',
+    viper:
+      'M680 161q2 42-2 63.5T659 268q-13 18-22.5 25t-22.5 9q-8 1-30 1h-12q-17 53-22 87-4 31 2 56 3 16 15 48 18 44 26 79 27 69-3 274 27-22 45-57 16-30 27-74 7-26 18-80 9-46 14-67 9-36 19-61 8-14 33 10 29-111 12-195-16-80-78-162zm15 140q13 49 15 74 2 35-8 69-25 0-38 17-11 15-15 44-3 17-3 57l-1 23-3 8q-4 12-6 15-3 5-3-5 2-44-4-78-4-20-14.5-49T602 430q-2-28 8-67 11 0 29-8t32-21q18-15 24-33zM316 161q-2 42 2.5 63.5T337 268q13 18 23 25t22 9q9 1 31 1h12q17 53 21 87 4 31-1 56-3 16-16 48-17 44-26 79-27 69 4 274-27-22-46-57-15-30-27-74-7-26-18-80-8-46-14-67-8-36-19-61-8-14-33 10-28-111-12-195 16-80 78-162zm-15 140q-12 49-14 74-3 35 7 69 26 0 39 17 11 15 14 44 3 17 4 57v23l3 8q5 12 6 15 3 5 3-5-2-44 5-78 3-20 13.5-49t12.5-46q3-28-7-67-12 0-29.5-8T325 334q-17-15-24-33z',
+    pictomancer:
+      'M872 223q-18 33-56 96-49 82-63 112-4 10-4 28 1 11 0 18-2 31-10.5 47.5T721 531l-10-11q-26-31-40-42-24-17-56-23-21-3-20-13.5t17.5-23T651 397q4-2 10-4 13-3 25-10 15-12 36-36 12-15 39-49 52-66 80-93 16-11 25.5-6t5.5 24zM563 373q-18 0-31.5-9.5t-13.5-23 13.5-23 32-9.5 32 9.5 13.5 23-13.5 23T563 373zm-135 87q-26 0-45-13.5T364 414t19-32.5 45.5-13.5 45.5 13.5 19 32.5-19 32.5-46 13.5zm41-192q-64 31-116 79-58 53-84 110-29 65-13 126 6 20 17 38 7 11 21 27 16 19 21 30 7 17 2 41-14 36-42 35-70 0-105-111-25-79 5-159 28-78 100-140t171-91q12-2 21.5-.5t10 6-8.5 9.5zM364 584q-25 0-42-12.5T305 542t17-29.5 41.5-12.5 42 12.5T423 542t-17.5 29.5T364 584zm58 111q6-17 15-47 13-43 21-62 14-31 36-54 25-28 69-53 27-6 58 6 28 11 50.5 34t31.5 48q-7 85-64 142-53 52-132.5 68T348 768q1-12 11-22 6-5 22.5-14t24.5-14q12-10 16-23zm60 22q18 5 49-1 28-8 55-27 30-22 45-49 17-32 7-63-5-12-17.5-23T592 539q-19-4-29 5-83 71-81 173z',
+  },
+};
+
path: packages/ui-craft/react/ffxiv-job-icons-loop-using-flubber.tsx
user: login: coderabbitai[bot]
body: _🧹 Nitpick (assertive)_

**아이콘 경로 데이터를 별도 파일로 분리하는 방법을 고려해보세요**  
경로 데이터가 많아질 경우 수정이나 확장이 잦을 수 있으므로, 별도의 JSON 또는 모듈로 분리하면 유지보수할 때 더 편리할 수 있습니다.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-25T12:18:23Z
author_association: NONE

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-03-25T12:08:07Z
body: ###  ⚠️  No Changeset found

Latest commit: fbe66c69393565f8da09cb6f9ab0983a1bad636a

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/siluat/web-projects/new/ui-craft/ffxiv-job-icons-loop-using-flubber?filename=.changeset/light-kiwis-pull.md&value=---%0A%22%40siluat%2Fui-craft%22%3A%20patch%0A%22ui-craft-playground%22%3A%20patch%0A---%0A%0A%E2%9C%A8%20(ui-craft)%20Add%20FFXIV%20job%20icons%20loop%20component%20using%20flubber%0A)


author_association: NONE
user: login: coderabbitai[bot]
created_at: 2025-03-25T12:08:12Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough
이 풀 리퀘스트는 FFXIV 직업 아이콘 애니메이션에 flubber 라이브러리를 적용하기 위한 여러 변경 사항을 담고 있습니다. `packages/ui-craft/package.json`에 새로운 의존성과 타입 정의가 추가되었고, 새 React 컴포넌트와 연관 훅(`useIncrementingTuple`)이 구현되었습니다. 또한, 해당 컴포넌트의 기능을 검증하는 테스트 케이스가 추가되었으며, playground의 임포트 및 텍스트도 업데이트되었습니다.

## Changes

| 파일(들) | 변경 요약 |
| -------- | --------- |
| `packages/ui-craft/package.json` | 새로운 devDependency(`@types/flubber`)와 dependencies(`flubber`, `framer-motion`) 추가 |
| `packages/ui-craft/react/ffxiv-job-icons-loop-using-flubber.tsx`<br>`packages/ui-craft/react/ffxiv-job-icons-loop-using-flubber.test.tsx` | FFXIV 직업 아이콘 애니메이션을 구현하는 React 컴포넌트와 관련 훅, 테스트 케이스 추가 |
| `playgrounds/ui-craft-playground/app/page.tsx` | 기존 `FfxivJobIconsLoopUsingGooeyEffect` 임포트를 `FfxivJobIconsLoopUsingFlubber`로 교체 및 텍스트 업데이트 |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant C as FfxivJobIconsLoopUsingFlubber
    participant I as useIncrementingTuple
    participant S as useSvgPathMorphing
    participant F as flubber
    participant M as framer-motion

    C->>I: 인덱스 튜플 요청
    I-->>C: 현재 튜플 반환
    C->>S: SVG 경로 변환 시작
    S->>F: 현재와 다음 경로 사이 보간 함수 생성
    F-->>S: 보간 함수 제공
    S->>M: 애니메이션 실행 요청
    M-->>C: 업데이트된 SVG 경로 제공
```

## Possibly related PRs

- **siluat/web-projects#10**: FFXIV 직업 아이콘 변환 컴포넌트 추가와 관련된 코드 변경으로, 이번 PR의 flubber 기반 애니메이션 구현과 밀접한 연관이 있음.

## Poem
> 나는 귀여운 토끼, 코드 숲 속을 달리며  
> 새로운 애니메이션이 춤추는 걸 바라보네.  
> flubber와 framer-motion, 마법의 짝꿍들이여,  
> 깡충깡충 뛰며 길을 밝히는 우리의 코드!  
> 🐇✨ 변화를 노래하며, 즐거운 발자국 남기네.  
> Hop, hop, Hop!  
> 코드 속에 봄바람이 불어오네!

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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= -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details>
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=siluat/web-projects&utm_content=13):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai plan` to trigger planning for file edits and PR creation.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE