diff --git a/frontend/public/components-metadata.json b/frontend/public/components-metadata.json
index e175f37..f9c895e 100644
--- a/frontend/public/components-metadata.json
+++ b/frontend/public/components-metadata.json
@@ -700,6 +700,33 @@
"segment"
]
},
+ {
+ "id": "74hc595",
+ "tagName": "wokwi-74hc595",
+ "name": "74HC595 Shift Register",
+ "category": "other",
+ "thumbnail": "",
+ "properties": [
+ {
+ "name": "values",
+ "type": "number",
+ "defaultValue": [0, 0, 0, 0, 0, 0, 0, 0],
+ "control": "range"
+ }
+ ],
+ "defaultValues": {
+ "values": [0, 0, 0, 0, 0, 0, 0, 0]
+ },
+ "pinCount": 16,
+ "tags": [
+ "74hc595",
+ "shift register",
+ "ic",
+ "chip",
+ "74",
+ "595"
+ ]
+ },
{
"id": "analog-joystick",
"tagName": "wokwi-analog-joystick",
diff --git a/frontend/src/components/components-wokwi/IC74HC595.ts b/frontend/src/components/components-wokwi/IC74HC595.ts
new file mode 100644
index 0000000..578e02c
--- /dev/null
+++ b/frontend/src/components/components-wokwi/IC74HC595.ts
@@ -0,0 +1,127 @@
+/**
+ * wokwi-74hc595 — 8-bit Serial-to-Parallel Shift Register (SN74HC595)
+ *
+ * DIP-16 package custom element for use in Velxio/Wokwi simulations.
+ */
+
+class IC74HC595Element extends HTMLElement {
+ private _values: number[] = [0, 0, 0, 0, 0, 0, 0, 0]; // Q0-Q7
+ private _shadow: ShadowRoot;
+
+ // ── Pin layout (DIP-16, matching wokwi-elements coordinate style) ─────────
+ // Top row (y=3): pins 16→9, left to right
+ // Bottom row (y=51): pins 1→8, left to right
+ readonly pinInfo = [
+ // Bottom row: pins 1-8 (Q1–Q7, GND)
+ { name: 'Q1', number: 1, x: 8.1, y: 51.3, signals: [] },
+ { name: 'Q2', number: 2, x: 17.7, y: 51.3, signals: [] },
+ { name: 'Q3', number: 3, x: 27.3, y: 51.3, signals: [] },
+ { name: 'Q4', number: 4, x: 36.9, y: 51.3, signals: [] },
+ { name: 'Q5', number: 5, x: 46.5, y: 51.3, signals: [] },
+ { name: 'Q6', number: 6, x: 56.1, y: 51.3, signals: [] },
+ { name: 'Q7', number: 7, x: 65.7, y: 51.3, signals: [] },
+ { name: 'GND', number: 8, x: 75.3, y: 51.3, signals: [] },
+ // Top row: pins 9-16 (Q7S, MR, SHCP, STCP, OE, DS, Q0, VCC) right to left
+ { name: 'Q7S', number: 9, x: 75.3, y: 3, signals: [] },
+ { name: 'MR', number: 10, x: 65.7, y: 3, signals: [] },
+ { name: 'SHCP', number: 11, x: 56.1, y: 3, signals: [] },
+ { name: 'STCP', number: 12, x: 46.5, y: 3, signals: [] },
+ { name: 'OE', number: 13, x: 36.9, y: 3, signals: [] },
+ { name: 'DS', number: 14, x: 27.3, y: 3, signals: [] },
+ { name: 'Q0', number: 15, x: 17.7, y: 3, signals: [] },
+ { name: 'VCC', number: 16, x: 8.1, y: 3, signals: [] },
+ ];
+
+ constructor() {
+ super();
+ this._shadow = this.attachShadow({ mode: 'open' });
+ this._render();
+ }
+
+ get values() {
+ return this._values;
+ }
+
+ set values(v: number[]) {
+ this._values = Array.isArray(v) ? [...v] : [0, 0, 0, 0, 0, 0, 0, 0];
+ this._renderOutputDots();
+ }
+
+ private _render() {
+ const PIN_X = [8.1, 17.7, 27.3, 36.9, 46.5, 56.1, 65.7, 75.3];
+ const TOP_Y = 3;
+ const BOT_Y = 51.3;
+ const CHIP_Y1 = 9;
+ const CHIP_H = 37;
+
+ const topLabels = ['VCC', 'Q0', 'DS', 'OE', 'SCP', 'HCP', 'MR', 'Q7S'];
+ const botLabels = ['Q1', 'Q2', 'Q3', 'Q4', 'Q5', 'Q6', 'Q7', 'GND'];
+
+ const pinLines = PIN_X.map((x) => `
+
+
+
+
+ `).join('');
+
+ const topText = PIN_X.map((x, i) => `
+ ${topLabels[i]}
+ `).join('');
+
+ const botText = PIN_X.map((x, i) => `
+ ${botLabels[i]}
+ `).join('');
+
+ const outputDots = PIN_X.map((x, i) => `
+
+ `).join('');
+
+ this._shadow.innerHTML = `
+
+
+ `;
+ }
+
+ private _renderOutputDots() {
+ if (!this._shadow) return;
+ // Update Q1-Q7 output dots (bottom, indices 1-7 of values)
+ for (let i = 1; i <= 7; i++) {
+ const dot = this._shadow.getElementById(`dot-q${i}`) as SVGCircleElement | null;
+ if (dot) {
+ dot.setAttribute('fill', this._values[i] ? '#00ff88' : '#333');
+ dot.setAttribute('opacity', this._values[i] ? '1' : '0.5');
+ }
+ }
+ // Q0 on top side
+ const dotQ0 = this._shadow.getElementById('dot-q0') as SVGCircleElement | null;
+ if (dotQ0) {
+ dotQ0.setAttribute('fill', this._values[0] ? '#00ff88' : '#333');
+ dotQ0.setAttribute('opacity', this._values[0] ? '1' : '0.5');
+ }
+ }
+}
+
+if (!customElements.get('wokwi-74hc595')) {
+ customElements.define('wokwi-74hc595', IC74HC595Element);
+}
+
+export { IC74HC595Element };
diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx
index 65cf9e4..a1f9ecc 100644
--- a/frontend/src/main.tsx
+++ b/frontend/src/main.tsx
@@ -1,5 +1,6 @@
import { createRoot } from 'react-dom/client'
import './index.css'
+import './components/components-wokwi/IC74HC595'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
diff --git a/frontend/src/simulation/parts/ChipParts.ts b/frontend/src/simulation/parts/ChipParts.ts
new file mode 100644
index 0000000..4307595
--- /dev/null
+++ b/frontend/src/simulation/parts/ChipParts.ts
@@ -0,0 +1,210 @@
+/**
+ * ChipParts.ts — Simulation logic for complex IC chips
+ *
+ * Implements:
+ * - 74HC595 8-bit Serial-to-Parallel Shift Register
+ * - wokwi-7segment display (driven by 74HC595 outputs)
+ */
+
+import { PartSimulationRegistry } from './PartSimulationRegistry';
+import { useSimulatorStore } from '../../store/useSimulatorStore';
+
+// ─── Helpers ─────────────────────────────────────────────────────────────────
+
+/**
+ * Given a 74HC595 component ID and a pin name (e.g. 'Q0'), find the DOM element
+ * of whatever component is connected on the other side of that wire, plus the
+ * pin name on that component.
+ */
+function getConnectedToPin(
+ componentId: string,
+ pinName: string,
+): { element: HTMLElement; pinName: string } | null {
+ const { wires } = useSimulatorStore.getState();
+ for (const wire of wires) {
+ let otherCompId: string | null = null;
+ let otherPin: string | null = null;
+
+ if (wire.start.componentId === componentId && wire.start.pinName === pinName) {
+ otherCompId = wire.end.componentId;
+ otherPin = wire.end.pinName;
+ } else if (wire.end.componentId === componentId && wire.end.pinName === pinName) {
+ otherCompId = wire.start.componentId;
+ otherPin = wire.start.pinName;
+ }
+
+ if (otherCompId && otherPin) {
+ const el = document.getElementById(otherCompId);
+ if (el) return { element: el as HTMLElement, pinName: otherPin };
+ }
+ }
+ return null;
+}
+
+/**
+ * Update a 7-segment display element when pin states change.
+ * pinName is the segment identifier (A, B, C, D, E, F, G, DP).
+ * state is whether the segment is lit (HIGH = lit for common-cathode).
+ */
+function set7SegPin(element: HTMLElement, pinName: string, state: boolean) {
+ const segmentIndex: Record = {
+ A: 0, B: 1, C: 2, D: 3, E: 4, F: 5, G: 6, DP: 7,
+ };
+ const idx = segmentIndex[pinName.toUpperCase()];
+ if (idx === undefined) return;
+
+ const el = element as any;
+ const current: number[] = Array.isArray(el.values) ? [...el.values] : [0, 0, 0, 0, 0, 0, 0, 0];
+ current[idx] = state ? 1 : 0;
+ el.values = current;
+}
+
+// ─── 74HC595 simulation ───────────────────────────────────────────────────────
+
+PartSimulationRegistry.register('74hc595', {
+ attachEvents: (element, simulator, getArduinoPinHelper) => {
+ const pinManager = (simulator as any).pinManager;
+ if (!pinManager) return () => {};
+
+ // Internal state
+ let shiftReg = 0; // 8-bit shift register
+ let storageReg = 0; // 8-bit storage register (output)
+ let oeActive = false; // output enable (active low)
+ let mrActive = true; // master reset (active low — HIGH = not reset)
+
+ let prevShcp = false;
+ let prevStcp = false;
+
+ // Resolve connected Arduino pins
+ const pinDS = getArduinoPinHelper('DS');
+ const pinSHCP = getArduinoPinHelper('SHCP');
+ const pinSTCP = getArduinoPinHelper('STCP');
+ const pinMR = getArduinoPinHelper('MR');
+ const pinOE = getArduinoPinHelper('OE');
+
+ const unsubscribers: (() => void)[] = [];
+
+ // Helper: propagate current storage reg outputs to connected components
+ const propagateOutputs = () => {
+ if (!oeActive) return; // outputs disabled (OE high = disabled)
+ const compId = element.id;
+
+ // Q0-Q7 maps to bits 0-7 of storageReg
+ const outputPins = ['Q0', 'Q1', 'Q2', 'Q3', 'Q4', 'Q5', 'Q6', 'Q7'];
+ for (let i = 0; i < 8; i++) {
+ const state = ((storageReg >> i) & 1) === 1;
+ const connected = getConnectedToPin(compId, outputPins[i]);
+ if (connected) {
+ // Update 7-segment or LED or any other component
+ const tagName = connected.element.tagName.toLowerCase();
+ if (tagName === 'wokwi-7segment') {
+ set7SegPin(connected.element, connected.pinName, state);
+ } else if (tagName === 'wokwi-led') {
+ (connected.element as any).value = state ? 1 : 0;
+ }
+ // Update 74HC595 chained via Q7S
+ if (tagName === 'wokwi-74hc595' && outputPins[i] === 'Q7S') {
+ // Q7S is serial out — drives DS of next chip (handled via wire logic)
+ }
+ }
+ }
+
+ // Update this element's visual (Q0-Q7 output dots)
+ const el = element as any;
+ el.values = outputPins.map((_, i) => ((storageReg >> i) & 1));
+
+ // Also propagate Q7S (serial output = bit 7 of shift register, not storage)
+ const q7sConn = getConnectedToPin(element.id, 'Q7S');
+ if (q7sConn) {
+ // Q7S is used to chain to the DS pin of next 74HC595 — this is handled
+ // by the DS monitoring of the downstream chip
+ }
+ };
+
+ // OE (active low — LOW enables outputs)
+ if (pinOE !== null) {
+ pinManager.triggerPinChange(pinOE, true); // default HIGH = disabled
+ unsubscribers.push(pinManager.onPinChange(pinOE, (_: number, state: boolean) => {
+ oeActive = !state; // OE low = active
+ propagateOutputs();
+ }));
+ } else {
+ oeActive = true; // assume OE tied to GND (always enabled)
+ }
+
+ // MR (active low — LOW resets shift register)
+ if (pinMR !== null) {
+ unsubscribers.push(pinManager.onPinChange(pinMR, (_: number, state: boolean) => {
+ mrActive = state; // MR high = no reset, low = reset
+ if (!mrActive) {
+ shiftReg = 0;
+ }
+ }));
+ } else {
+ mrActive = true; // assume MR tied high
+ }
+
+ // DS — latched on SHCP rising edge; just track current value
+ let dsState = false;
+ if (pinDS !== null) {
+ unsubscribers.push(pinManager.onPinChange(pinDS, (_: number, state: boolean) => {
+ dsState = state;
+ }));
+ }
+
+ // SHCP — rising edge shifts DS into shift register
+ if (pinSHCP !== null) {
+ unsubscribers.push(pinManager.onPinChange(pinSHCP, (_: number, state: boolean) => {
+ if (state && !prevShcp) {
+ // Rising edge
+ if (mrActive) {
+ // Shift: MSB shifts out via Q7S, DS enters at LSB
+ shiftReg = ((shiftReg << 1) | (dsState ? 1 : 0)) & 0xFF;
+ }
+ }
+ prevShcp = state;
+ }));
+ }
+
+ // STCP — rising edge latches shift register to storage register
+ if (pinSTCP !== null) {
+ unsubscribers.push(pinManager.onPinChange(pinSTCP, (_: number, state: boolean) => {
+ if (state && !prevStcp) {
+ // Rising edge — latch
+ storageReg = shiftReg;
+ propagateOutputs();
+ }
+ prevStcp = state;
+ }));
+ }
+
+ // Initial state propagation
+ propagateOutputs();
+
+ return () => unsubscribers.forEach(u => u());
+ },
+});
+
+// ─── 7-segment display (direct-drive, when connected directly to Arduino) ────
+
+PartSimulationRegistry.register('7segment', {
+ attachEvents: (element, simulator, getArduinoPinHelper) => {
+ const pinManager = (simulator as any).pinManager;
+ if (!pinManager) return () => {};
+
+ const unsubscribers: (() => void)[] = [];
+
+ const segments = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'DP'];
+ for (let i = 0; i < segments.length; i++) {
+ const seg = segments[i];
+ const arduinoPin = getArduinoPinHelper(seg);
+ if (arduinoPin !== null) {
+ unsubscribers.push(pinManager.onPinChange(arduinoPin, (_: number, state: boolean) => {
+ set7SegPin(element, seg, state);
+ }));
+ }
+ }
+
+ return () => unsubscribers.forEach(u => u());
+ },
+});
diff --git a/frontend/src/simulation/parts/index.ts b/frontend/src/simulation/parts/index.ts
index e7a1d14..c7f2b58 100644
--- a/frontend/src/simulation/parts/index.ts
+++ b/frontend/src/simulation/parts/index.ts
@@ -1,3 +1,4 @@
export * from './PartSimulationRegistry';
import './BasicParts';
import './ComplexParts';
+import './ChipParts';
diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts
index 650a52f..553d84a 100644
--- a/frontend/src/vite-env.d.ts
+++ b/frontend/src/vite-env.d.ts
@@ -23,5 +23,6 @@ declare namespace JSX {
'wokwi-ir-receiver': any;
'wokwi-pir-motion-sensor': any;
'wokwi-photoresistor-sensor': any;
+ 'wokwi-74hc595': any;
}
}